cfn-vpn 0.5.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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