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
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'