cfn-vpn 0.5.1 → 1.1.0

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