cfn-vpn 0.5.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build-gem.yml +25 -0
  3. data/.github/workflows/release-gem.yml +34 -0
  4. data/.github/workflows/release-image.yml +33 -0
  5. data/Gemfile.lock +33 -39
  6. data/README.md +1 -247
  7. data/cfn-vpn.gemspec +4 -4
  8. data/docs/README.md +44 -0
  9. data/docs/certificate-users.md +89 -0
  10. data/docs/getting-started.md +128 -0
  11. data/docs/modifying.md +67 -0
  12. data/docs/routes.md +98 -0
  13. data/docs/scheduling.md +32 -0
  14. data/docs/sessions.md +27 -0
  15. data/lib/cfnvpn.rb +31 -27
  16. data/lib/cfnvpn/{client.rb → actions/client.rb} +5 -6
  17. data/lib/cfnvpn/{embedded.rb → actions/embedded.rb} +15 -15
  18. data/lib/cfnvpn/actions/init.rb +144 -0
  19. data/lib/cfnvpn/actions/modify.rb +169 -0
  20. data/lib/cfnvpn/actions/params.rb +73 -0
  21. data/lib/cfnvpn/{revoke.rb → actions/revoke.rb} +6 -6
  22. data/lib/cfnvpn/actions/routes.rb +196 -0
  23. data/lib/cfnvpn/{sessions.rb → actions/sessions.rb} +5 -5
  24. data/lib/cfnvpn/{share.rb → actions/share.rb} +10 -10
  25. data/lib/cfnvpn/actions/subnets.rb +78 -0
  26. data/lib/cfnvpn/certificates.rb +5 -5
  27. data/lib/cfnvpn/clientvpn.rb +49 -65
  28. data/lib/cfnvpn/compiler.rb +23 -0
  29. data/lib/cfnvpn/config.rb +34 -78
  30. data/lib/cfnvpn/{cloudformation.rb → deployer.rb} +47 -19
  31. data/lib/cfnvpn/log.rb +26 -26
  32. data/lib/cfnvpn/s3.rb +34 -4
  33. data/lib/cfnvpn/s3_bucket.rb +48 -0
  34. data/lib/cfnvpn/string.rb +33 -0
  35. data/lib/cfnvpn/templates/helper.rb +14 -0
  36. data/lib/cfnvpn/templates/lambdas.rb +35 -0
  37. data/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py +175 -0
  38. data/lib/cfnvpn/templates/lambdas/scheduler/app.py +36 -0
  39. data/lib/cfnvpn/templates/vpn.rb +449 -0
  40. data/lib/cfnvpn/version.rb +1 -1
  41. metadata +73 -23
  42. data/lib/cfnvpn/cfhighlander.rb +0 -49
  43. data/lib/cfnvpn/init.rb +0 -109
  44. data/lib/cfnvpn/modify.rb +0 -103
  45. data/lib/cfnvpn/routes.rb +0 -84
  46. data/lib/cfnvpn/templates/cfnvpn.cfhighlander.rb.tt +0 -27
@@ -6,7 +6,7 @@ require 'cfnvpn/log'
6
6
 
7
7
  module CfnVpn
8
8
  class Certificates
9
- include CfnVpn::Log
9
+
10
10
 
11
11
  def initialize(build_dir, cfnvpn_name, easyrsa_local = false)
12
12
  @cfnvpn_name = cfnvpn_name
@@ -44,7 +44,7 @@ module CfnVpn
44
44
  @docker_cmd << "-v #{@cert_dir}:/easy-rsa/output"
45
45
  @docker_cmd << @easyrsa_image
46
46
  @docker_cmd << "sh -c 'create-ca'"
47
- Log.logger.debug `#{@docker_cmd.join(' ')}`
47
+ CfnVpn::Log.logger.debug `#{@docker_cmd.join(' ')}`
48
48
  end
49
49
  end
50
50
 
@@ -59,7 +59,7 @@ module CfnVpn
59
59
  @docker_cmd << "-v #{@cert_dir}:/easy-rsa/output"
60
60
  @docker_cmd << @easyrsa_image
61
61
  @docker_cmd << "sh -c 'create-client'"
62
- Log.logger.debug `#{@docker_cmd.join(' ')}`
62
+ CfnVpn::Log.logger.debug `#{@docker_cmd.join(' ')}`
63
63
  end
64
64
  end
65
65
 
@@ -76,7 +76,7 @@ module CfnVpn
76
76
  @docker_cmd << "-v #{@cert_dir}:/easy-rsa/output"
77
77
  @docker_cmd << @easyrsa_image
78
78
  @docker_cmd << "sh -c 'revoke-client'"
79
- Log.logger.debug `#{@docker_cmd.join(' ')}`
79
+ CfnVpn::Log.logger.debug `#{@docker_cmd.join(' ')}`
80
80
  end
81
81
  end
82
82
 
@@ -84,7 +84,7 @@ module CfnVpn
84
84
  cn = cn.nil? ? cert : cn
85
85
  acm = CfnVpn::Acm.new(region, @cert_dir)
86
86
  arn = acm.import_certificate("#{cert}.crt", "#{cert}.key", "ca.crt")
87
- Log.logger.debug "Uploaded #{type} certificate to ACM #{arn}"
87
+ CfnVpn::Log.logger.debug "Uploaded #{type} certificate to ACM #{arn}"
88
88
  acm.tag_certificate(arn,cn,type,@cfnvpn_name)
89
89
  return arn
90
90
  end
@@ -4,11 +4,12 @@ require 'netaddr'
4
4
 
5
5
  module CfnVpn
6
6
  class ClientVpn
7
- include CfnVpn::Log
7
+
8
8
 
9
9
  def initialize(name,region)
10
10
  @client = Aws::EC2::Client.new(region: region)
11
11
  @name = name
12
+ @endpoint_id = self.get_endpoint_id()
12
13
  end
13
14
 
14
15
  def get_endpoint()
@@ -16,7 +17,7 @@ module CfnVpn
16
17
  filters: [{ name: "tag:cfnvpn:name", values: [@name] }]
17
18
  })
18
19
  if resp.client_vpn_endpoints.empty?
19
- Log.logger.error "unable to find endpoint with tag Key: cfnvpn:name with Value: #{@name}"
20
+ CfnVpn::Log.logger.error "unable to find endpoint with tag Key: cfnvpn:name with Value: #{@name}"
20
21
  raise "Unable to find client vpn"
21
22
  end
22
23
  return resp.client_vpn_endpoints.first
@@ -68,86 +69,69 @@ module CfnVpn
68
69
  })
69
70
  end
70
71
 
71
- def get_target_networks(endpoint_id)
72
- resp = @client.describe_client_vpn_target_networks({
73
- client_vpn_endpoint_id: endpoint_id
74
- })
75
- return resp.client_vpn_target_networks.first
76
- end
77
-
78
- def add_route(cidr,description)
72
+ def get_routes()
79
73
  endpoint_id = get_endpoint_id()
80
- subnet_id = get_target_networks(endpoint_id).target_network_id
81
-
82
- @client.create_client_vpn_route({
74
+ resp = @client.describe_client_vpn_routes({
83
75
  client_vpn_endpoint_id: endpoint_id,
84
- destination_cidr_block: cidr,
85
- target_vpc_subnet_id: subnet_id,
86
- description: description
76
+ max_results: 20
87
77
  })
78
+ return resp.routes
79
+ end
88
80
 
89
- resp = @client.authorize_client_vpn_ingress({
90
- client_vpn_endpoint_id: endpoint_id,
91
- target_network_cidr: cidr,
92
- authorize_all_groups: true,
93
- description: description
81
+ def get_groups_for_route(endpoint, cidr)
82
+ auth_resp = @client.describe_client_vpn_authorization_rules({
83
+ client_vpn_endpoint_id: endpoint,
84
+ filters: [
85
+ {
86
+ name: 'destination-cidr',
87
+ values: [cidr]
88
+ }
89
+ ]
94
90
  })
95
-
96
- return resp.status
91
+ return auth_resp.authorization_rules.map {|rule| rule.group_id }
97
92
  end
98
93
 
99
- def del_route(cidr)
100
- endpoint_id = get_endpoint_id()
101
- subnet_id = get_target_networks(endpoint_id).target_network_id
102
-
103
- revoke = @client.revoke_client_vpn_ingress({
104
- revoke_all_groups: true,
105
- client_vpn_endpoint_id: endpoint_id,
106
- target_network_cidr: cidr
94
+ def get_associations(endpoint)
95
+ associations = []
96
+ resp = @client.describe_client_vpn_target_networks({
97
+ client_vpn_endpoint_id: endpoint
107
98
  })
108
99
 
109
- route = @client.delete_client_vpn_route({
110
- client_vpn_endpoint_id: endpoint_id,
111
- target_vpc_subnet_id: subnet_id,
112
- destination_cidr_block: cidr
113
- })
100
+ resp.client_vpn_target_networks.each do |net|
101
+ subnet_resp = @client.describe_subnets({
102
+ subnet_ids: [net.target_network_id]
103
+ })
104
+ subnet = subnet_resp.subnets.first
105
+ groups = get_groups_for_route(endpoint, subnet.cidr_block)
106
+
107
+ associations.push({
108
+ association_id: net.association_id,
109
+ target_network_id: net.target_network_id,
110
+ status: net.status.code,
111
+ cidr: subnet.cidr_block,
112
+ az: subnet.availability_zone,
113
+ groups: groups.join(' ')
114
+ })
115
+ end
114
116
 
115
- return route.status, revoke.status
117
+ return associations
116
118
  end
117
119
 
118
- def get_routes()
119
- endpoint_id = get_endpoint_id()
120
- resp = @client.describe_client_vpn_routes({
121
- client_vpn_endpoint_id: endpoint_id,
122
- max_results: 20
120
+ def delete_route(cidr, subnet)
121
+ @client.delete_client_vpn_route({
122
+ client_vpn_endpoint_id: @endpoint_id,
123
+ target_vpc_subnet_id: subnet,
124
+ destination_cidr_block: cidr
123
125
  })
124
- return resp.routes
125
- end
126
-
127
- def route_exists?(cidr)
128
- routes = get_routes()
129
- resp = routes.select { |route| route if route.destination_cidr == cidr }
130
- return resp.any?
131
126
  end
132
127
 
133
- def get_routes()
128
+ def revoke_auth(cidr)
134
129
  endpoint_id = get_endpoint_id()
135
- resp = @client.describe_client_vpn_routes({
136
- client_vpn_endpoint_id: endpoint_id,
137
- max_results: 20
130
+ @client.revoke_client_vpn_ingress({
131
+ client_vpn_endpoint_id: @endpoint_id,
132
+ target_network_cidr: cidr,
133
+ revoke_all_groups: true
138
134
  })
139
- return resp.routes
140
- end
141
-
142
- def get_route_with_mask()
143
- routes = get_routes()
144
- routes
145
- .select { |r| r if r.destination_cidr != '0.0.0.0/0' }
146
- .collect { |r| { route: r.destination_cidr.split('/').first, mask: NetAddr::IPv4Net.parse(r.destination_cidr).netmask.extended }}
147
- end
148
-
149
- def valid_cidr?(cidr)
150
- return !(cidr =~ /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/).nil?
151
135
  end
152
136
 
153
137
  end
@@ -0,0 +1,23 @@
1
+ require 'cfnvpn/log'
2
+ require 'cfnvpn/templates/vpn'
3
+
4
+ module CfnVpn
5
+ class Compiler
6
+
7
+ def initialize(name, config)
8
+ @name = name
9
+ @config = config
10
+ end
11
+
12
+ def compile
13
+ CfnVpn::Log.logger.debug "Compiling cloudformation"
14
+ template = CfnVpn::Templates::Vpn.new()
15
+ template.render(@name, @config)
16
+ CfnVpn::Log.logger.debug "Validating cloudformation"
17
+ valid = template.validate
18
+ CfnVpn::Log.logger.debug "Clouformation Template\n\n#{JSON.parse(valid.to_json).to_yaml}"
19
+ return JSON.parse(valid.to_json).to_yaml
20
+ end
21
+
22
+ end
23
+ end
data/lib/cfnvpn/config.rb CHANGED
@@ -1,89 +1,45 @@
1
- require 'cfnvpn/clientvpn'
2
- require 'cfnvpn/log'
3
- require 'cfnvpn/globals'
1
+ require 'aws-sdk-ssm'
2
+ require 'json'
3
+ require 'cfnvpn/deployer'
4
4
 
5
5
  module CfnVpn
6
- class Config < Thor::Group
7
- include Thor::Actions
8
- include CfnVpn::Log
9
-
10
- argument :name
11
-
12
- class_option :profile, desc: 'AWS Profile'
13
- class_option :region, default: ENV['AWS_REGION'], desc: 'AWS Region'
14
- class_option :verbose, desc: 'set log level to debug', type: :boolean
15
- class_option :bucket, required: true, desc: 's3 bucket'
16
- class_option :client_cn, required: true, desc: "client certificates to download"
17
- class_option :easyrsa_local, type: :boolean, default: false, desc: 'run the easyrsa executable from your local rather than from docker'
18
- class_option :ignore_routes, alias: :i, type: :boolean, desc: "Ignore client VPN pushed routes and set routes in config file"
19
-
20
- def self.source_root
21
- File.dirname(__FILE__)
22
- end
23
-
24
- def set_loglevel
25
- Log.logger.level = Logger::DEBUG if @options['verbose']
26
- end
27
-
28
- def create_config_directory
29
- @build_dir = "#{CfnVpn.cfnvpn_path}/#{@name}"
30
- @config_dir = "#{@build_dir}/config"
31
- Log.logger.debug("Creating config directory #{@config_dir}")
32
- FileUtils.mkdir_p(@config_dir)
33
- end
34
-
35
- def download_config
36
- vpn = CfnVpn::ClientVpn.new(@name,@options['region'])
37
- @endpoint_id = vpn.get_endpoint_id()
38
- Log.logger.info "downloading client config for #{@endpoint_id}"
39
- @config = vpn.get_config(@endpoint_id)
40
- end
41
-
42
- def download_certificates
43
- download = true
44
- if File.exists?("#{@config_dir}/#{@options['client_cn']}.crt")
45
- download = yes? "Certificates for #{@options['client_cn']} already exist in #{@config_dir}. Do you want to download again? ", :green
46
- end
47
-
48
- if download
49
- Log.logger.info "Downloading certificates for #{@options['client_cn']} to #{@config_dir}"
50
- s3 = CfnVpn::S3.new(@options['region'],@options['bucket'],@name)
51
- s3.get_object("#{@config_dir}/#{@options['client_cn']}.tar.gz")
52
- cert = CfnVpn::Certificates.new(@build_dir,@name,@options['easyrsa_local'])
53
- Log.logger.debug cert.extract_certificate(@options['client_cn'])
6
+ class Config
7
+
8
+ def self.get_config(region, name)
9
+ client = Aws::SSM::Client.new(region: region)
10
+ begin
11
+ resp = client.get_parameter({name: "/cfnvpn/config/#{name}"})
12
+ return JSON.parse(resp.parameter.value, {:symbolize_names => true})
13
+ rescue Aws::SSM::Errors::ParameterNotFound => e
14
+ return self.get_config_from_parameter(region, name)
54
15
  end
55
16
  end
56
17
 
57
- def alter_config
58
- string = (0...8).map { (65 + rand(26)).chr.downcase }.join
59
- @config.sub!(@endpoint_id, "#{string}.#{@endpoint_id}")
60
- @config.concat("\n\ncert #{@config_dir}/#{@options['client_cn']}.crt")
61
- @config.concat("\nkey #{@config_dir}/#{@options['client_cn']}.key\n")
18
+ # to support upgrade from <=0.5.1 to >1.0
19
+ def self.get_config_from_parameter(region, name)
20
+ deployer = CfnVpn::Deployer.new(region, name)
21
+ parameters = deployer.get_parameters_from_stack_hash()
22
+ {
23
+ type: 'certificate',
24
+ server_cert_arn: parameters[:ServerCertificateArn],
25
+ client_cert_arn: parameters[:ClientCertificateArn],
26
+ region: region,
27
+ subnet_ids: [parameters[:AssociationSubnetId]],
28
+ cidr: parameters[:ClientCidrBlock],
29
+ dns_servers: parameters[:DnsServers].split(','),
30
+ split_tunnel: parameters[:SplitTunnel] == "true",
31
+ internet_route: parameters[:InternetRoute] == "true" ? parameters[:AssociationSubnetId] : nil,
32
+ protocol: parameters[:Protocol],
33
+ routes: []
34
+ }
62
35
  end
63
36
 
64
- def add_routes
65
- if @options['ignore_routes']
66
- Log.logger.debug "Ignoring routes pushed by the client vpn"
67
- @config.concat("\nroute-nopull\n")
68
- vpn = CfnVpn::ClientVpn.new(@name,@options['region'])
69
- routes = vpn.get_route_with_mask
70
- Log.logger.debug "Found routes #{routes}"
71
- routes.each do |r|
72
- @config.concat("route #{r[:route]} #{r[:mask]}\n")
73
- end
74
- dns_servers = vpn.get_dns_servers()
75
- if dns_servers.any?
76
- Log.logger.debug "Found DNS servers #{dns_servers.join(' ')}"
77
- @config.concat("dhcp-option DNS #{dns_servers.first}\n")
78
- end
79
- end
37
+ def self.get_config_from_yaml_file(file)
38
+ YAML.load(File.read(file), symbolize_names: true)
80
39
  end
81
40
 
82
- def write_config
83
- config_file = "#{@config_dir}/#{@name}.ovpn"
84
- File.write(config_file, @config)
85
- Log.logger.info "downloaded client config #{config_file}"
41
+ def self.dump_config_to_yaml_file(name, params)
42
+ File.write(File.join(Dir.pwd, "cfnvpn.#{name}.yaml"), Hash[params.collect{|k,v| [k.to_s, v]}].to_yaml)
86
43
  end
87
-
88
44
  end
89
- end
45
+ end
@@ -2,10 +2,10 @@ require 'aws-sdk-cloudformation'
2
2
  require 'fileutils'
3
3
  require 'cfnvpn/version'
4
4
  require 'cfnvpn/log'
5
+ require 'cfnvpn/string'
5
6
 
6
7
  module CfnVpn
7
- class Cloudformation
8
- include CfnVpn::Log
8
+ class Deployer
9
9
 
10
10
  def initialize(region,name)
11
11
  @name = name
@@ -29,28 +29,25 @@ module CfnVpn
29
29
  return does_cf_stack_exist() ? 'UPDATE' : 'CREATE'
30
30
  end
31
31
 
32
- def create_change_set(template_path,parameters)
32
+ def create_change_set(template_body: nil, parameters: {})
33
33
  change_set_name = "#{@stack_name}-#{CfnVpn::CHANGE_SET_VERSION}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"
34
34
  change_set_type = get_change_set_type()
35
35
 
36
- if change_set_type == 'CREATE'
37
- params = get_parameters_from_template(template_path)
36
+ if !template_body.nil?
37
+ params = get_parameters_from_template(template_body)
38
38
  else
39
39
  params = get_parameters_from_stack()
40
40
  end
41
-
41
+
42
42
  params.each do |param|
43
43
  if !parameters[param[:parameter_key]].nil?
44
44
  param[:parameter_value] = parameters[param[:parameter_key]]
45
45
  param[:use_previous_value] = false
46
46
  end
47
47
  end
48
-
49
- template_body = File.read(template_path)
50
- Log.logger.debug "Creating changeset"
51
- change_set = @client.create_change_set({
48
+
49
+ changeset_args = {
52
50
  stack_name: @stack_name,
53
- template_body: template_body,
54
51
  parameters: params,
55
52
  tags: [
56
53
  {
@@ -63,18 +60,28 @@ module CfnVpn
63
60
  }
64
61
  ],
65
62
  change_set_name: change_set_name,
66
- change_set_type: change_set_type
67
- })
63
+ change_set_type: change_set_type,
64
+ capabilities: ['CAPABILITY_IAM']
65
+ }
66
+
67
+ if !template_body.nil?
68
+ changeset_args[:template_body] = template_body
69
+ else
70
+ changeset_args[:use_previous_template] = true
71
+ end
72
+
73
+ CfnVpn::Log.logger.debug "Creating changeset"
74
+ change_set = @client.create_change_set(changeset_args)
68
75
  return change_set, change_set_type
69
76
  end
70
77
 
71
78
  def wait_for_changeset(change_set_id)
72
- Log.logger.debug "Waiting for changeset to be created"
79
+ CfnVpn::Log.logger.debug "Waiting for changeset to be created"
73
80
  begin
74
81
  @client.wait_until :change_set_create_complete, change_set_name: change_set_id
75
82
  rescue Aws::Waiters::Errors::FailureStateError => e
76
83
  change_set = get_change_set(change_set_id)
77
- Log.logger.error("change set status: #{change_set.status} reason: #{change_set.status_reason}")
84
+ CfnVpn::Log.logger.error("change set status: #{change_set.status} reason: #{change_set.status_reason}")
78
85
  exit 1
79
86
  end
80
87
  end
@@ -86,7 +93,7 @@ module CfnVpn
86
93
  end
87
94
 
88
95
  def execute_change_set(change_set_id)
89
- Log.logger.debug "Executing the changeset"
96
+ CfnVpn::Log.logger.debug "Executing the changeset"
90
97
  stack = @client.execute_change_set({
91
98
  change_set_name: change_set_id
92
99
  })
@@ -94,7 +101,7 @@ module CfnVpn
94
101
 
95
102
  def wait_for_execute(change_set_type)
96
103
  waiter = change_set_type == 'CREATE' ? :stack_create_complete : :stack_update_complete
97
- Log.logger.info "Waiting for changeset to #{change_set_type}"
104
+ CfnVpn::Log.logger.info "Waiting for changeset to #{change_set_type}"
98
105
  resp = @client.wait_until waiter, stack_name: @stack_name
99
106
  end
100
107
 
@@ -103,11 +110,32 @@ module CfnVpn
103
110
  return resp.parameters.collect { |p| { parameter_key: p.parameter_key, use_previous_value: true } }
104
111
  end
105
112
 
106
- def get_parameters_from_template(template_path)
107
- template_body = File.read(template_path)
113
+ def get_parameters_from_template(template_body)
108
114
  resp = @client.get_template_summary({ template_body: template_body })
109
115
  return resp.parameters.collect { |p| { parameter_key: p.parameter_key, parameter_value: p.default_value } }
110
116
  end
111
117
 
118
+ def get_parameter_value(parameter)
119
+ resp = @client.describe_stacks({ stack_name: @stack_name })
120
+ stack = resp.stacks.first
121
+ parameter = stack.parameters.detect {|p| p.parameter_key == parameter}
122
+ return parameter ? parameter.parameter_value : nil
123
+ end
124
+
125
+ def get_parameters_from_stack_hash()
126
+ resp = @client.describe_stacks({
127
+ stack_name: @stack_name,
128
+ })
129
+ stack = resp.stacks.first
130
+ return Hash[stack.parameters.collect {|parameter| [parameter.parameter_key.to_sym, parameter.parameter_value]}]
131
+ end
132
+
133
+ def get_outputs_from_stack()
134
+ resp = @client.describe_stacks({
135
+ stack_name: @stack_name,
136
+ })
137
+ stack = resp.stacks.first
138
+ return Hash[stack.outputs.collect {|output| [output.output_key.underscore.to_sym, output.output_value]}]
139
+ end
112
140
  end
113
141
  end