diff --git a/.github/workflows/build-gem.yml b/.github/workflows/build-gem.yml new file mode 100644 index 0000000..d8ff219 --- /dev/null +++ b/.github/workflows/build-gem.yml @@ -0,0 +1,28 @@ +name: test and build gem +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + name: test + build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up ruby 2.7 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + + - name: rspec + run: | + gem install rspec + rspec + + - name: build gem + run: | + gem build cfn-vpn.gemspec \ No newline at end of file diff --git a/.github/workflows/release-gem.yml b/.github/workflows/release-gem.yml new file mode 100644 index 0000000..1b49f4a --- /dev/null +++ b/.github/workflows/release-gem.yml @@ -0,0 +1,34 @@ +name: release gem + +on: + release: + types: [published] + +jobs: + build: + name: Build + Publish Gem + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Set up ruby 2.7 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + + - name: rspec + run: | + gem install rspec + rspec + + - name: build gem + run: | + gem build cfn-vpn.gemspec + + - name: Publish gem + uses: dawidd6/action-publish-gem@v1 + with: + api_key: ${{secrets.RUBYGEMS_API_KEY}} + github_token: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml new file mode 100644 index 0000000..6cf36c9 --- /dev/null +++ b/.github/workflows/release-image.yml @@ -0,0 +1,33 @@ +name: release docker image + +on: + release: + types: [published] + +jobs: + build: + name: Build + Publish Container Image + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to GitHub Container Repository + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_PUSH_TOKEN }} + + - name: Build and push Container Image to GitHub Container Repository + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + push: true + tags: ghcr.io/base2services/cfnvpn:${{ github.event.release.tag_name }} + build-args: CFNVPN_VERSION=${{ github.event.release.tag_name }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 63e8a15..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -sudo: required -dist: trusty -language: ruby -rvm: - - 2.5 -script: - - bundle install - - gem build cfn-vpn.gemspec - - gem install cfn-vpn-*.gem - - cfn-vpn help -deploy: - provider: rubygems - api_key: "${RUBYGEMS_API_KEY}" - gem: cfn-vpn - on: - all_branches: true - condition: $TRAVIS_BRANCH =~ ^develop|master && $TRAVIS_EVENT_TYPE =~ ^push|api$ && $TRAVIS_REPO_SLUG == "base2services/aws-client-vpn" diff --git a/Dockerfile b/Dockerfile index 7d47ee0..bf7bee8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,15 @@ -FROM ruby:2.7-alpine +FROM ruby:2.7 -RUN apk add --no-cache easy-rsa git \ - # Hack until easy-rsa 3.0.7 is released https://github.com/OpenVPN/easy-rsa/issues/261 - && sed -i 's/^RANDFILE\s*=\s\$ENV.*/#&/' /usr/share/easy-rsa/openssl-easyrsa.cnf \ +RUN apt-get update -qq \ + && apt-get install -qqy \ + easy-rsa \ + git \ && ln -s /usr/share/easy-rsa/easyrsa /usr/bin/ ENV EASYRSA=/usr/share/easy-rsa ENV EASYRSA_BATCH=yes -ARG CFNVPN_VERSION="0.5.0" +ARG CFNVPN_VERSION="1.5.0" COPY . /src @@ -17,9 +18,9 @@ WORKDIR /src RUN gem build cfn-vpn.gemspec \ && gem install cfn-vpn-${CFNVPN_VERSION}.gem \ && rm -rf /src - -RUN addgroup -g 1000 cfnvpn && \ - adduser -D -u 1000 -G cfnvpn cfnvpn + +RUN addgroup --gid 1000 cfnvpn && \ + adduser --home /home/cfnvpn --uid 1000 --disabled-password --gecos GECOS --gid 1000 cfnvpn USER cfnvpn diff --git a/Gemfile.lock b/Gemfile.lock index c585884..f0521f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,66 +1,75 @@ PATH remote: . specs: - cfn-vpn (0.2.0) + cfn-vpn (0.5.0) aws-sdk-acm (~> 1, < 2) aws-sdk-cloudformation (~> 1, < 2) aws-sdk-ec2 (~> 1.95, < 2) aws-sdk-s3 (~> 1, < 2) cfhighlander (~> 0.9, < 1) - cfndsl (~> 0.17, < 1) + netaddr (= 2.0.4) terminal-table (~> 1, < 2) thor (~> 0.20) GEM remote: https://rubygems.org/ specs: - aws-eventstream (1.0.3) - aws-partitions (1.253.0) - aws-sdk-acm (1.23.0) - aws-sdk-core (~> 3, >= 3.56.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + aws-eventstream (1.2.0) + aws-partitions (1.707.0) + aws-sdk-acm (1.55.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-cloudformation (1.29.0) - aws-sdk-core (~> 3, >= 3.71.0) + aws-sdk-cloudformation (1.76.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.85.1) - aws-eventstream (~> 1.0, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-sdk-core (3.170.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-ec2 (1.364.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-ec2 (1.124.0) - aws-sdk-core (~> 3, >= 3.71.0) + aws-sdk-kms (1.62.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-kms (1.27.0) - aws-sdk-core (~> 3, >= 3.71.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.59.0) - aws-sdk-core (~> 3, >= 3.83.0) + aws-sdk-s3 (1.119.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.0) - aws-eventstream (~> 1.0, >= 1.0.2) - cfhighlander (0.10.7) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + cfhighlander (0.12.8) aws-sdk-cloudformation (~> 1, < 2) aws-sdk-core (~> 3, < 4) aws-sdk-ec2 (~> 1, < 2) aws-sdk-s3 (~> 1, < 2) - cfndsl (= 0.17.2) + cfndsl (~> 1.3, < 2) duplicate (~> 1.1) git (~> 1.4, < 2) highline (>= 1.7.10, < 1.8) rubyzip (>= 2.0.0, < 3) thor (~> 0.20, < 1) - cfndsl (0.17.2) + cfndsl (1.6.0) + hana (~> 1.3) duplicate (1.1.1) - git (1.5.0) + git (1.13.2) + addressable (~> 2.8) + rchardet (~> 1.8) + hana (1.3.7) highline (1.7.10) - jmespath (1.4.0) + jmespath (1.6.2) + netaddr (2.0.4) + public_suffix (5.0.1) rake (10.5.0) - rubyzip (2.0.0) + rchardet (1.8.0) + rubyzip (2.3.2) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thor (0.20.3) - unicode-display_width (1.6.0) + unicode-display_width (1.8.0) PLATFORMS ruby @@ -71,4 +80,4 @@ DEPENDENCIES rake (~> 10.0) BUNDLED WITH - 2.0.1 + 2.3.13 diff --git a/lib/cfnvpn.rb b/lib/cfnvpn.rb index b8766d7..6573455 100644 --- a/lib/cfnvpn.rb +++ b/lib/cfnvpn.rb @@ -9,6 +9,7 @@ require 'cfnvpn/routes' require 'cfnvpn/share' require 'cfnvpn/embedded' +require 'cfnvpn/renew_certificate' module CfnVpn class Cli < Thor @@ -21,6 +22,9 @@ def __print_version register CfnVpn::Init, 'init', 'init [name]', 'Create a AWS Client VPN' tasks["init"].options = CfnVpn::Init.class_options + + register CfnVpn::RenewCertificate, 'renew', 'renew [name]', 'Create a AWS Client VPN' + tasks["renew"].options = CfnVpn::RenewCertificate.class_options register CfnVpn::Modify, 'modify', 'modify [name]', 'Modify your AWS Client VPN' tasks["modify"].options = CfnVpn::Modify.class_options diff --git a/lib/cfnvpn/certificates.rb b/lib/cfnvpn/certificates.rb index 64e8a5c..13ccf15 100644 --- a/lib/cfnvpn/certificates.rb +++ b/lib/cfnvpn/certificates.rb @@ -48,6 +48,65 @@ def generate_ca(server_cn,client_cn) end end + def renew(server_cn,client_cn,expiry=nil) + opts = "" + unless expiry.nil? + opts += "--days=#{expiry}" + end + + if @easyrsa_local + ENV["EASYRSA_REQ_CN"] = server_cn + ENV["EASYRSA_PKI"] = @pki_dir + system("tar xzfv #{@cert_dir}/ca.tar.gz --directory #{@build_dir}") + system("easyrsa #{opts} renew server nopass") + system("easyrsa #{opts} renew #{client_cn} nopass") + FileUtils.cp(["#{@pki_dir}/ca.crt", "#{@pki_dir}/issued/server.crt", "#{@pki_dir}/private/server.key", "#{@pki_dir}/issued/#{client_cn}.crt", "#{@pki_dir}/private/#{client_cn}.key"], @cert_dir) + system("tar czfv #{@cert_dir}/ca.tar.gz -C #{@build_dir} pki/") + else + @docker_cmd << "-e EASYRSA_REQ_CN=#{server_cn}" + @docker_cmd << "-e EASYRSA_CLIENT_CN=#{client_cn}" + @docker_cmd << "-e EASYRSA_OPTS=\"#{opts}\"" + @docker_cmd << "-v #{@cert_dir}:/easy-rsa/output" + @docker_cmd << @easyrsa_image + @docker_cmd << "sh -c 'renew'" + CfnVpn::Log.logger.debug `#{@docker_cmd.join(' ')}` + end + end + + def rebuild(server_cn,client_cn,expiry=nil) + timestamp = Time.now.getutc.to_i + opts = "" + unless expiry.nil? + opts += "--days=#{expiry}" + end + + if @easyrsa_local + ENV["EASYRSA_REQ_CN"] = server_cn + ENV["EASYRSA_PKI"] = @pki_dir + system("tar xzfv #{@cert_dir}/ca.tar.gz --directory #{@build_dir}") + + FileUtils.mv("#{@pki_dir}/reqs/server.req", "#{@pki_dir}/reqs/server.req.bak-#{timestamp}") + FileUtils.mv("#{@pki_dir}/issued/server.crt", "#{@pki_dir}/issued/server.req.bak-#{timestamp}") + FileUtils.mv("#{@pki_dir}/private/server.key", "#{@pki_dir}/private/server.req.bak-#{timestamp}") + FileUtils.mv("#{@pki_dir}/reqs/#{client_cn}.req", "#{@pki_dir}/reqs/#{client_cn}.req.bak-#{timestamp}") + FileUtils.mv("#{@pki_dir}/issued/#{client_cn}.crt", "#{@pki_dir}/issued/#{client_cn}.req.bak-#{timestamp}") + FileUtils.mv("#{@pki_dir}/private/#{client_cn}.key", "#{@pki_dir}/private/#{client_cn}.req.bak-#{timestamp}") + + system("easyrsa #{opts} build-server-full server nopass") + system("easyrsa #{opts} build-client-full #{client_cn} nopass") + FileUtils.cp(["#{@pki_dir}/ca.crt", "#{@pki_dir}/issued/server.crt", "#{@pki_dir}/private/server.key", "#{@pki_dir}/issued/#{client_cn}.crt", "#{@pki_dir}/private/#{client_cn}.key"], @cert_dir) + system("tar czfv #{@cert_dir}/ca.tar.gz -C #{@build_dir} pki/") + else + @docker_cmd << "-e EASYRSA_REQ_CN=#{server_cn}" + @docker_cmd << "-e EASYRSA_CLIENT_CN=#{client_cn}" + @docker_cmd << "-e EASYRSA_OPTS=\"#{opts}\"" + @docker_cmd << "-v #{@cert_dir}:/easy-rsa/output" + @docker_cmd << @easyrsa_image + @docker_cmd << "sh -c 'rebuild'" + CfnVpn::Log.logger.debug `#{@docker_cmd.join(' ')}` + end + end + def generate_client(client_cn) if @easyrsa_local ENV["EASYRSA_PKI"] = @pki_dir diff --git a/lib/cfnvpn/renew_certificate.rb b/lib/cfnvpn/renew_certificate.rb new file mode 100644 index 0000000..59cff4d --- /dev/null +++ b/lib/cfnvpn/renew_certificate.rb @@ -0,0 +1,120 @@ +require 'thor' +require 'fileutils' +require 'cfnvpn/cloudformation' +require 'cfnvpn/certificates' +require 'cfnvpn/cfhighlander' +require 'cfnvpn/cloudformation' +require 'cfnvpn/log' +require 'cfnvpn/clientvpn' +require 'cfnvpn/globals' + +module CfnVpn + class RenewCertificate < Thor::Group + include Thor::Actions + include CfnVpn::Log + + argument :name + + class_option :profile, aliases: :p, desc: 'AWS Profile' + class_option :region, aliases: :r, default: ENV['AWS_REGION'], desc: 'AWS Region' + class_option :verbose, desc: 'set log level to debug', type: :boolean + + class_option :server_cn, required: true, desc: 'server certificate common name' + class_option :client_cn, desc: 'client certificate common name' + class_option :easyrsa_local, type: :boolean, default: false, desc: 'run the easyrsa executable from your local rather than from docker' + class_option :certificate_expiry, type: :string, desc: 'value in days for when the server certificates expire, defaults to 825 days' + class_option :rebuild, type: :boolean, default: false, desc: 'generates new certificates from the existing CA for certiciate type VPNs' + class_option :bucket, required: true, desc: 's3 bucket' + + def self.source_root + File.dirname(__FILE__) + end + + def set_loglevel + Log.logger.level = Logger::DEBUG if @options['verbose'] + end + + def create_build_directory + @build_dir = "#{CfnVpn.cfnvpn_path}/#{@name}" + Log.logger.debug "creating directory #{@build_dir}" + FileUtils.mkdir_p(@build_dir) + end + + def initialize_config + @config = {} + @config['parameters'] = {} + @config['template_version'] = '0.2.0' + end + + def stack_exist + @cfn = CfnVpn::Cloudformation.new(@options['region'],@name) + if !@cfn.does_cf_stack_exist() + Log.logger.error "#{@name}-cfnvpn stack doesn't exists in this account in region #{@options['region']}\n Try running `cfn-vpn init #{@name}` to setup the stack" + exit 1 + end + end + + def set_client_cn + @client_cn = @options['client_cn'] ? @options['client_cn'] : "client-vpn.#{@options['server_cn']}" + end + + # create certificates + def generate_server_certificates + cert = CfnVpn::Certificates.new(@build_dir,@name,@options['easyrsa_local']) + if @options['rebuild'] + Log.logger.info "rebuilding certificates using openvpn easy-rsa" + cert.rebuild(@options['server_cn'],@client_cn,@options['certificate_expiry']) + else + Log.logger.info "rebuilding certificates using openvpn easy-rsa" + cert.renew(@options['server_cn'],@client_cn,@options['certificate_expiry']) + end + end + + def upload_certificates + cert = CfnVpn::Certificates.new(@build_dir,@name,@options['easyrsa_local']) + @config['parameters']['ServerCertificateArn'] = cert.upload_certificates(@options['region'],'server','server',@options['server_cn']) + @config['parameters']['ClientCertificateArn'] = cert.upload_certificates(@options['region'],@client_cn,'client') + s3 = CfnVpn::S3.new(@options['region'],@options['bucket'],@name) + s3.store_object("#{@build_dir}/certificates/ca.tar.gz") + end + + def deploy_vpn + template('templates/cfnvpn.cfhighlander.rb.tt', "#{@build_dir}/#{@name}.cfhighlander.rb", @config, force: true) + Log.logger.debug "Generating cloudformation from #{@build_dir}/#{@name}.cfhighlander.rb" + cfhl = CfnVpn::CfHiglander.new(@options['region'],@name,@config,@build_dir) + template_path = cfhl.render() + Log.logger.debug "Cloudformation template #{template_path} generated and validated" + + Log.logger.info "Modifying cloudformation stack #{@name}-cfnvpn in #{@options['region']}" + cfn = CfnVpn::Cloudformation.new(@options['region'],@name) + change_set, change_set_type = cfn.create_change_set(template_path,@config['parameters']) + cfn.wait_for_changeset(change_set.id) + changes = cfn.get_change_set(change_set.id) + + Log.logger.warn("The following changes to the cfnvpn stack will be made") + changes.changes.each do |change| + Log.logger.warn("ID: #{change.resource_change.logical_resource_id} Action: #{change.resource_change.action}") + change.resource_change.details.each do |details| + Log.logger.warn("Name: #{details.target.name} Attribute: #{details.target.attribute} Cause: #{details.causing_entity}") + end + end + + continue = yes? "Continue?", :green + if !continue + Log.logger.error("Cancelled cfn-vpn modifiy #{@name}") + exit 1 + end + + cfn.execute_change_set(change_set.id) + cfn.wait_for_execute(change_set_type) + Log.logger.debug "Changeset #{change_set_type} complete" + end + + def finish + vpn = CfnVpn::ClientVpn.new(@name,@options['region']) + @endpoint_id = vpn.get_endpoint_id() + Log.logger.info "Client VPN #{@endpoint_id} modified." + end + + end +end \ No newline at end of file diff --git a/lib/cfnvpn/version.rb b/lib/cfnvpn/version.rb index 7bdfd38..933bcf2 100644 --- a/lib/cfnvpn/version.rb +++ b/lib/cfnvpn/version.rb @@ -1,4 +1,4 @@ module CfnVpn - VERSION = "0.5.0".freeze + VERSION = "0.5.0.1".freeze CHANGE_SET_VERSION = VERSION.gsub('.', '-').freeze end