cfn-vpn 0.5.1 → 1.1.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build-gem.yml +25 -0
  3. data/.github/workflows/release-gem.yml +31 -0
  4. data/.github/workflows/release-image.yml +33 -0
  5. data/Gemfile.lock +30 -38
  6. data/README.md +1 -247
  7. data/cfn-vpn.gemspec +3 -2
  8. data/docs/README.md +44 -0
  9. data/docs/certificate-users.md +89 -0
  10. data/docs/getting-started.md +87 -0
  11. data/docs/modifying.md +67 -0
  12. data/docs/routes.md +82 -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 +130 -0
  19. data/lib/cfnvpn/actions/modify.rb +149 -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 +144 -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 +34 -68
  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 +4 -4
  33. data/lib/cfnvpn/string.rb +29 -0
  34. data/lib/cfnvpn/templates/helper.rb +14 -0
  35. data/lib/cfnvpn/templates/vpn.rb +344 -0
  36. data/lib/cfnvpn/version.rb +1 -1
  37. metadata +55 -22
  38. data/lib/cfnvpn/cfhighlander.rb +0 -49
  39. data/lib/cfnvpn/init.rb +0 -109
  40. data/lib/cfnvpn/modify.rb +0 -103
  41. data/lib/cfnvpn/routes.rb +0 -84
  42. data/lib/cfnvpn/templates/cfnvpn.cfhighlander.rb.tt +0 -27
@@ -4,7 +4,7 @@ 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)
@@ -16,7 +16,7 @@ module CfnVpn
16
16
  filters: [{ name: "tag:cfnvpn:name", values: [@name] }]
17
17
  })
18
18
  if resp.client_vpn_endpoints.empty?
19
- Log.logger.error "unable to find endpoint with tag Key: cfnvpn:name with Value: #{@name}"
19
+ CfnVpn::Log.logger.error "unable to find endpoint with tag Key: cfnvpn:name with Value: #{@name}"
20
20
  raise "Unable to find client vpn"
21
21
  end
22
22
  return resp.client_vpn_endpoints.first
@@ -68,53 +68,6 @@ module CfnVpn
68
68
  })
69
69
  end
70
70
 
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)
79
- endpoint_id = get_endpoint_id()
80
- subnet_id = get_target_networks(endpoint_id).target_network_id
81
-
82
- @client.create_client_vpn_route({
83
- client_vpn_endpoint_id: endpoint_id,
84
- destination_cidr_block: cidr,
85
- target_vpc_subnet_id: subnet_id,
86
- description: description
87
- })
88
-
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
94
- })
95
-
96
- return resp.status
97
- end
98
-
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
107
- })
108
-
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
- })
114
-
115
- return route.status, revoke.status
116
- end
117
-
118
71
  def get_routes()
119
72
  endpoint_id = get_endpoint_id()
120
73
  resp = @client.describe_client_vpn_routes({
@@ -124,30 +77,43 @@ module CfnVpn
124
77
  return resp.routes
125
78
  end
126
79
 
127
- def route_exists?(cidr)
128
- routes = get_routes()
129
- resp = routes.select { |route| route if route.destination_cidr == cidr }
130
- return resp.any?
80
+ def get_groups_for_route(endpoint, cidr)
81
+ auth_resp = @client.describe_client_vpn_authorization_rules({
82
+ client_vpn_endpoint_id: endpoint,
83
+ filters: [
84
+ {
85
+ name: 'destination-cidr',
86
+ values: [cidr]
87
+ }
88
+ ]
89
+ })
90
+ return auth_resp.authorization_rules.map {|rule| rule.group_id }
131
91
  end
132
92
 
133
- def get_routes()
134
- endpoint_id = get_endpoint_id()
135
- resp = @client.describe_client_vpn_routes({
136
- client_vpn_endpoint_id: endpoint_id,
137
- max_results: 20
93
+ def get_associations(endpoint)
94
+ associations = []
95
+ resp = @client.describe_client_vpn_target_networks({
96
+ client_vpn_endpoint_id: endpoint
138
97
  })
139
- return resp.routes
140
- end
141
98
 
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
99
+ resp.client_vpn_target_networks.each do |net|
100
+ subnet_resp = @client.describe_subnets({
101
+ subnet_ids: [net.target_network_id]
102
+ })
103
+ subnet = subnet_resp.subnets.first
104
+ groups = get_groups_for_route(endpoint, subnet.cidr_block)
105
+
106
+ associations.push({
107
+ association_id: net.association_id,
108
+ target_network_id: net.target_network_id,
109
+ status: net.status.code,
110
+ cidr: subnet.cidr_block,
111
+ az: subnet.availability_zone,
112
+ groups: groups.join(' ')
113
+ })
114
+ end
148
115
 
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?
116
+ return associations
151
117
  end
152
118
 
153
119
  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
data/lib/cfnvpn/log.rb CHANGED
@@ -1,38 +1,38 @@
1
1
  require 'logger'
2
2
 
3
3
  module CfnVpn
4
- module Log
5
-
6
- def self.colors
7
- @colors ||= {
8
- ERROR: 31, # red
9
- WARN: 33, # yellow
10
- INFO: 0,
11
- DEBUG: 32 # grenn
12
- }
13
- end
4
+ module Log
5
+ class << self
6
+ def colors
7
+ @colors ||= {
8
+ ERROR: 31, # red
9
+ WARN: 33, # yellow
10
+ INFO: 0,
11
+ DEBUG: 32 # grenn
12
+ }
13
+ end
14
14
 
15
- def self.logger
16
- if @logger.nil?
17
- @logger = Logger.new(STDOUT)
18
- @logger.level = Logger::INFO
19
- @logger.formatter = proc do |severity, datetime, progname, msg|
20
- "\e[#{colors[severity.to_sym]}m#{severity}: - #{msg}\e[0m\n"
15
+ def logger
16
+ if @logger.nil?
17
+ @logger = Logger.new(STDOUT)
18
+ @logger.level = Logger::INFO
19
+ @logger.formatter = proc do |severity, datetime, progname, msg|
20
+ "\e[#{colors[severity.to_sym]}m#{severity}: - #{msg}\e[0m\n"
21
+ end
21
22
  end
23
+ @logger
22
24
  end
23
- @logger
24
- end
25
25
 
26
- def self.logger=(logger)
27
- @logger = logger
28
- end
26
+ def logger=(logger)
27
+ @logger = logger
28
+ end
29
29
 
30
- levels = %w(debug info warn error fatal)
31
- levels.each do |level|
32
- define_method("#{level.to_sym}") do |msg|
33
- self.logger.send(level, msg)
30
+ levels = %w(debug info warn error fatal)
31
+ levels.each do |level|
32
+ define_method("#{level.to_sym}") do |msg|
33
+ self.logger.send(level, msg)
34
+ end
34
35
  end
35
36
  end
36
-
37
37
  end
38
38
  end