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
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
data/lib/cfnvpn/s3.rb CHANGED
@@ -14,7 +14,7 @@ module CfnVpn
14
14
  def store_object(file)
15
15
  body = File.open(file, 'rb').read
16
16
  file_name = file.split('/').last
17
- Log.logger.debug("uploading #{file} to s3://#{@bucket}/#{@path}/#{file_name}")
17
+ CfnVpn::Log.logger.debug("uploading #{file} to s3://#{@bucket}/#{@path}/#{file_name}")
18
18
  @client.put_object({
19
19
  body: body,
20
20
  bucket: @bucket,
@@ -26,7 +26,7 @@ module CfnVpn
26
26
 
27
27
  def get_object(file)
28
28
  file_name = file.split('/').last
29
- Log.logger.debug("downloading s3://#{@bucket}/#{@path}/#{file_name} to #{file}")
29
+ CfnVpn::Log.logger.debug("downloading s3://#{@bucket}/#{@path}/#{file_name} to #{file}")
30
30
  @client.get_object(
31
31
  response_target: file,
32
32
  bucket: @bucket,
@@ -34,7 +34,7 @@ module CfnVpn
34
34
  end
35
35
 
36
36
  def store_config(config)
37
- Log.logger.debug("uploading config to s3://#{@bucket}/#{@path}/#{@name}.config.ovpn")
37
+ CfnVpn::Log.logger.debug("uploading config to s3://#{@bucket}/#{@path}/#{@name}.config.ovpn")
38
38
  @client.put_object({
39
39
  body: config,
40
40
  bucket: @bucket,
@@ -54,7 +54,7 @@ module CfnVpn
54
54
  end
55
55
 
56
56
  def store_embedded_config(config, cn)
57
- Log.logger.debug("uploading config to s3://#{@bucket}/#{@path}/#{@name}_#{cn}.config.ovpn")
57
+ CfnVpn::Log.logger.debug("uploading config to s3://#{@bucket}/#{@path}/#{@name}_#{cn}.config.ovpn")
58
58
  @client.put_object({
59
59
  body: config,
60
60
  bucket: @bucket,
@@ -63,5 +63,35 @@ module CfnVpn
63
63
  })
64
64
  end
65
65
 
66
+ def create_bucket
67
+ @client.create_bucket({
68
+ bucket: bucket,
69
+ acl: 'private'
70
+ })
71
+
72
+ @client.put_public_access_block({
73
+ bucket: bucket,
74
+ public_access_block_configuration: {
75
+ block_public_acls: true,
76
+ ignore_public_acls: true,
77
+ block_public_policy: true,
78
+ restrict_public_buckets: true,
79
+ }
80
+ })
81
+
82
+ @client.put_bucket_encryption({
83
+ bucket: bucket,
84
+ server_side_encryption_configuration: {
85
+ rules: [
86
+ {
87
+ apply_server_side_encryption_by_default: {
88
+ sse_algorithm: "AES256"
89
+ }
90
+ }
91
+ ]
92
+ }
93
+ })
94
+ end
95
+
66
96
  end
67
97
  end
@@ -0,0 +1,48 @@
1
+ require 'aws-sdk-s3'
2
+ require 'fileutils'
3
+ require 'securerandom'
4
+
5
+ module CfnVpn
6
+ class S3Bucket
7
+
8
+ def initialize(region, name)
9
+ @client = Aws::S3::Client.new(region: region)
10
+ @name = name
11
+ end
12
+
13
+ def generate_bucket_name
14
+ return "cfnvpn-#{@name}-#{SecureRandom.hex}"
15
+ end
16
+
17
+ def create_bucket(bucket)
18
+ @client.create_bucket({
19
+ bucket: bucket,
20
+ acl: 'private'
21
+ })
22
+
23
+ @client.put_public_access_block({
24
+ bucket: bucket,
25
+ public_access_block_configuration: {
26
+ block_public_acls: true,
27
+ ignore_public_acls: true,
28
+ block_public_policy: true,
29
+ restrict_public_buckets: true,
30
+ }
31
+ })
32
+
33
+ @client.put_bucket_encryption({
34
+ bucket: bucket,
35
+ server_side_encryption_configuration: {
36
+ rules: [
37
+ {
38
+ apply_server_side_encryption_by_default: {
39
+ sse_algorithm: "AES256"
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ })
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ class String
2
+ def underscore
3
+ self.gsub(/::/, '/').
4
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
5
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
6
+ tr("-", "_").
7
+ downcase
8
+ end
9
+
10
+ def resource_safe
11
+ self.gsub(/[^a-zA-Z0-9]/, "").capitalize
12
+ end
13
+
14
+ def event_id_safe
15
+ self.gsub('*', 'wildcard').gsub(/[^\.\-_A-Za-z0-9]+/, "").downcase
16
+ end
17
+
18
+ def colorize(color_code)
19
+ "\e[#{color_code}m#{self}\e[0m"
20
+ end
21
+
22
+ def red
23
+ colorize(31)
24
+ end
25
+
26
+ def green
27
+ colorize(32)
28
+ end
29
+
30
+ def yellow
31
+ colorize(33)
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ require 'aws-sdk-ec2'
2
+
3
+ module CfnVpn
4
+ module Templates
5
+ class Helper
6
+ def self.get_auth_cidr(region, subnet_id)
7
+ client = Aws::EC2::Client.new(region: region)
8
+ subnets = client.describe_subnets({subnet_ids:[subnet_id]})
9
+ vpcs = client.describe_vpcs({vpc_ids:[subnets.subnets[0].vpc_id]})
10
+ return vpcs.vpcs[0].cidr_block
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,35 @@
1
+ require 'zip'
2
+ require 'securerandom'
3
+ require 'aws-sdk-s3'
4
+ require 'cfnvpn/log'
5
+
6
+ module CfnVpn
7
+ module Templates
8
+ class Lambdas
9
+
10
+ def self.package_lambda(name:, bucket:, func:, files:)
11
+ lambdas_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lambdas')
12
+ FileUtils.mkdir_p(lambdas_dir)
13
+
14
+ CfnVpn::Log.logger.debug "zipping lambda function #{func}"
15
+ zipfile_name = "#{func}-#{SecureRandom.hex}.zip"
16
+ zipfile_path = "#{CfnVpn.cfnvpn_path}/#{name}/lambdas"
17
+ FileUtils.mkdir_p(zipfile_path)
18
+ Zip::File.open("#{zipfile_path}/#{zipfile_name}", Zip::File::CREATE) do |zipfile|
19
+ files.each do |file|
20
+ zipfile.add(file, File.join("#{lambdas_dir}/#{func}", file))
21
+ end
22
+ end
23
+
24
+ bucket = Aws::S3::Bucket.new(bucket)
25
+ object = bucket.object("cfnvpn/lambdas/#{name}/#{zipfile_name}")
26
+ CfnVpn::Log.logger.debug "uploading #{zipfile_name} to s3://#{bucket}/#{object.key}"
27
+ object.upload_file("#{zipfile_path}/#{zipfile_name}")
28
+
29
+ return object.key
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,175 @@
1
+ import socket
2
+ import boto3
3
+ from botocore.exceptions import ClientError
4
+ import logging
5
+
6
+ logger = logging.getLogger()
7
+ logger.setLevel(logging.INFO)
8
+
9
+
10
+ def delete_route(client, vpn_endpoint, subnet, cidr):
11
+ client.delete_client_vpn_route(
12
+ ClientVpnEndpointId=vpn_endpoint,
13
+ TargetVpcSubnetId=subnet,
14
+ DestinationCidrBlock=cidr,
15
+ )
16
+
17
+
18
+ def create_route(client, event, cidr):
19
+ client.create_client_vpn_route(
20
+ ClientVpnEndpointId=event['ClientVpnEndpointId'],
21
+ DestinationCidrBlock=cidr,
22
+ TargetVpcSubnetId=event['TargetSubnet'],
23
+ Description=f"cfnvpn auto generated route for endpoint {event['Record']}. {event['Description']}"
24
+ )
25
+
26
+
27
+ def revoke_route_auth(client, event, cidr, group = None):
28
+ args = {
29
+ 'ClientVpnEndpointId': event['ClientVpnEndpointId'],
30
+ 'TargetNetworkCidr': cidr
31
+ }
32
+
33
+ if group is None:
34
+ args['RevokeAllGroups'] = True
35
+ else:
36
+ args['AccessGroupId'] = group
37
+
38
+ client.revoke_client_vpn_ingress(**args)
39
+
40
+
41
+ def authorize_route(client, event, cidr, group = None):
42
+ args = {
43
+ 'ClientVpnEndpointId': event['ClientVpnEndpointId'],
44
+ 'TargetNetworkCidr': cidr,
45
+ 'Description': f"cfnvpn auto generated authorization for endpoint {event['Record']}. {event['Description']}"
46
+ }
47
+
48
+ if group is None:
49
+ args['AuthorizeAllGroups'] = True
50
+ else:
51
+ args['AccessGroupId'] = group
52
+
53
+ client.authorize_client_vpn_ingress(**args)
54
+
55
+
56
+ def get_routes(client, event):
57
+ response = client.describe_client_vpn_routes(
58
+ ClientVpnEndpointId=event['ClientVpnEndpointId'],
59
+ Filters=[
60
+ {
61
+ 'Name': 'origin',
62
+ 'Values': ['add-route']
63
+ }
64
+ ]
65
+ )
66
+
67
+ routes = [route for route in response['Routes'] if event['Record'] in route['Description']]
68
+ logger.info(f"found {len(routes)} exisiting routes for {event['Record']}")
69
+ return routes
70
+
71
+
72
+ def get_rules(client, vpn_endpoint, cidr):
73
+ response = client.describe_client_vpn_authorization_rules(
74
+ ClientVpnEndpointId=vpn_endpoint,
75
+ Filters=[
76
+ {
77
+ 'Name': 'destination-cidr',
78
+ 'Values': [cidr]
79
+ }
80
+ ]
81
+ )
82
+ return response['AuthorizationRules']
83
+
84
+
85
+ def handler(event,context):
86
+
87
+ # DNS lookup on the dns record and return all IPS for the endpoint
88
+ try:
89
+ cidrs = [ ip + "/32" for ip in socket.gethostbyname_ex(event['Record'])[-1]]
90
+ logger.info(f"resolved endpoint {event['Record']} to {cidrs}")
91
+ except socket.gaierror as e:
92
+ logger.exception(f"failed to resolve record {event['Record']}")
93
+ return 'KO'
94
+
95
+ client = boto3.client('ec2')
96
+ routes = get_routes(client, event)
97
+
98
+ for cidr in cidrs:
99
+ route = next((route for route in routes if route['DestinationCidr'] == cidr), None)
100
+
101
+ # if there are no existing routes for the endpoint cidr create a new route
102
+ if route is None:
103
+ try:
104
+ create_route(client, event, cidr)
105
+ if 'Groups' in event:
106
+ for group in event['Groups']:
107
+ authorize_route(client, event, cidr, group)
108
+ else:
109
+ authorize_route(client, event, cidr)
110
+ except ClientError as e:
111
+ if e.response['Error']['Code'] == 'InvalidClientVpnDuplicateRoute':
112
+ logger.error(f"route for CIDR {cidr} already exists with a different endpoint")
113
+ continue
114
+ raise e
115
+
116
+ # if the route already exists
117
+ else:
118
+
119
+ logger.info(f"route for cidr {cidr} is already in place")
120
+
121
+ # if the target subnet has changed in the payload, recreate the routes to use the new subnet
122
+ if route['TargetSubnet'] != event['TargetSubnet']:
123
+ logger.info(f"target subnet for route for {cidr} has changed, recreating the route")
124
+ delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], cidr)
125
+ create_route(client, event, cidr)
126
+
127
+ logger.info(f"checking authorization rules for the route")
128
+
129
+ # check the rules match the payload
130
+ rules = get_rules(client, event['ClientVpnEndpointId'], cidr)
131
+ existing_groups = [rule['GroupId'] for rule in rules]
132
+ if 'Groups' in event:
133
+ # remove expired rules not defined in the payload anymore
134
+ expired_rules = [rule for rule in rules if rule['GroupId'] not in event['Groups']]
135
+ for rule in expired_rules:
136
+ logger.info(f"removing expired authorization rule for group {rule['GroupId']} for route {cidr}")
137
+ revoke_route_auth(client, event, cidr, rule['GroupId'])
138
+ # add new rules defined in the payload
139
+ new_rules = [group for group in event['Groups'] if group not in existing_groups]
140
+ for group in new_rules:
141
+ logger.info(f"creating new authorization rule for group {rule['GroupId']} for route {cidr}")
142
+ authorize_route(client, event, cidr, group)
143
+ else:
144
+ # if amount of rules for the cidr is greater than 1 when no groups are specified in the payload
145
+ # we'll assume that all groups have been removed from the payload so we'll remove all existing rules and add a rule for allow all
146
+ if len(rules) > 1:
147
+ logger.info(f"creating an allow all rule for route {cidr}")
148
+ revoke_route_auth(client, event, cidr)
149
+ authorize_route(client, event, cidr)
150
+
151
+
152
+
153
+
154
+ # clean up any expired routes when the ips for an endpoint change
155
+ expired_routes = [route for route in routes if route['DestinationCidr'] not in cidrs]
156
+ for route in expired_routes:
157
+ logger.info(f"removing expired route {route['DestinationCidr']} for endpoint {event['Record']}")
158
+
159
+ try:
160
+ revoke_route_auth(client, event, route['DestinationCidr'])
161
+ except ClientError as e:
162
+ if e.response['Error']['Code'] == 'InvalidClientVpnEndpointAuthorizationRuleNotFound':
163
+ pass
164
+ else:
165
+ raise e
166
+
167
+ try:
168
+ delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], route['DestinationCidr'])
169
+ except ClientError as e:
170
+ if e.response['Error']['Code'] == 'InvalidClientVpnRouteNotFound':
171
+ pass
172
+ else:
173
+ raise e
174
+
175
+ return 'OK'