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.
- checksums.yaml +4 -4
- data/.github/workflows/build-gem.yml +25 -0
- data/.github/workflows/release-gem.yml +34 -0
- data/.github/workflows/release-image.yml +33 -0
- data/Gemfile.lock +33 -39
- data/README.md +1 -247
- data/cfn-vpn.gemspec +4 -4
- data/docs/README.md +44 -0
- data/docs/certificate-users.md +89 -0
- data/docs/getting-started.md +128 -0
- data/docs/modifying.md +67 -0
- data/docs/routes.md +98 -0
- data/docs/scheduling.md +32 -0
- data/docs/sessions.md +27 -0
- data/lib/cfnvpn.rb +31 -27
- data/lib/cfnvpn/{client.rb → actions/client.rb} +5 -6
- data/lib/cfnvpn/{embedded.rb → actions/embedded.rb} +15 -15
- data/lib/cfnvpn/actions/init.rb +144 -0
- data/lib/cfnvpn/actions/modify.rb +169 -0
- data/lib/cfnvpn/actions/params.rb +73 -0
- data/lib/cfnvpn/{revoke.rb → actions/revoke.rb} +6 -6
- data/lib/cfnvpn/actions/routes.rb +196 -0
- data/lib/cfnvpn/{sessions.rb → actions/sessions.rb} +5 -5
- data/lib/cfnvpn/{share.rb → actions/share.rb} +10 -10
- data/lib/cfnvpn/actions/subnets.rb +78 -0
- data/lib/cfnvpn/certificates.rb +5 -5
- data/lib/cfnvpn/clientvpn.rb +49 -65
- data/lib/cfnvpn/compiler.rb +23 -0
- data/lib/cfnvpn/config.rb +34 -78
- data/lib/cfnvpn/{cloudformation.rb → deployer.rb} +47 -19
- data/lib/cfnvpn/log.rb +26 -26
- data/lib/cfnvpn/s3.rb +34 -4
- data/lib/cfnvpn/s3_bucket.rb +48 -0
- data/lib/cfnvpn/string.rb +33 -0
- data/lib/cfnvpn/templates/helper.rb +14 -0
- data/lib/cfnvpn/templates/lambdas.rb +35 -0
- data/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py +175 -0
- data/lib/cfnvpn/templates/lambdas/scheduler/app.py +36 -0
- data/lib/cfnvpn/templates/vpn.rb +449 -0
- data/lib/cfnvpn/version.rb +1 -1
- metadata +73 -23
- data/lib/cfnvpn/cfhighlander.rb +0 -49
- data/lib/cfnvpn/init.rb +0 -109
- data/lib/cfnvpn/modify.rb +0 -103
- data/lib/cfnvpn/routes.rb +0 -84
- 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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
26
|
+
def logger=(logger)
|
27
|
+
@logger = logger
|
28
|
+
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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'
|