cfn-vpn 0.4.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) 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/Dockerfile +26 -0
  6. data/Gemfile.lock +30 -38
  7. data/README.md +1 -232
  8. data/cfn-vpn.gemspec +3 -5
  9. data/docs/README.md +44 -0
  10. data/docs/certificate-users.md +89 -0
  11. data/docs/getting-started.md +128 -0
  12. data/docs/modifying.md +67 -0
  13. data/docs/routes.md +84 -0
  14. data/docs/scheduling.md +32 -0
  15. data/docs/sessions.md +27 -0
  16. data/lib/cfnvpn.rb +31 -27
  17. data/lib/cfnvpn/{client.rb → actions/client.rb} +11 -8
  18. data/lib/cfnvpn/{embedded.rb → actions/embedded.rb} +21 -19
  19. data/lib/cfnvpn/actions/init.rb +140 -0
  20. data/lib/cfnvpn/actions/modify.rb +149 -0
  21. data/lib/cfnvpn/actions/params.rb +73 -0
  22. data/lib/cfnvpn/{revoke.rb → actions/revoke.rb} +10 -8
  23. data/lib/cfnvpn/actions/routes.rb +144 -0
  24. data/lib/cfnvpn/{sessions.rb → actions/sessions.rb} +7 -6
  25. data/lib/cfnvpn/{share.rb → actions/share.rb} +10 -10
  26. data/lib/cfnvpn/actions/subnets.rb +78 -0
  27. data/lib/cfnvpn/certificates.rb +70 -20
  28. data/lib/cfnvpn/clientvpn.rb +34 -68
  29. data/lib/cfnvpn/compiler.rb +23 -0
  30. data/lib/cfnvpn/config.rb +34 -77
  31. data/lib/cfnvpn/{cloudformation.rb → deployer.rb} +48 -20
  32. data/lib/cfnvpn/globals.rb +16 -0
  33. data/lib/cfnvpn/log.rb +26 -26
  34. data/lib/cfnvpn/s3.rb +4 -4
  35. data/lib/cfnvpn/string.rb +29 -0
  36. data/lib/cfnvpn/templates/helper.rb +14 -0
  37. data/lib/cfnvpn/templates/vpn.rb +353 -0
  38. data/lib/cfnvpn/version.rb +1 -1
  39. metadata +56 -42
  40. data/lib/cfnvpn/cfhighlander.rb +0 -49
  41. data/lib/cfnvpn/init.rb +0 -107
  42. data/lib/cfnvpn/modify.rb +0 -102
  43. data/lib/cfnvpn/routes.rb +0 -83
  44. data/lib/cfnvpn/templates/cfnvpn.cfhighlander.rb.tt +0 -27
@@ -0,0 +1,16 @@
1
+ module CfnVpn
2
+ class << self
3
+
4
+ # Returns the filepath to the location CfnVpn will use for
5
+ # storage. Used for certificate generation as well as the
6
+ # download and upload location. Can be overridden by specifying
7
+ # a value for the ENV variable
8
+ # 'CFNVPN_PATH'.
9
+ #
10
+ # @return [String]
11
+ def cfnvpn_path
12
+ @cfnvpn_path ||= File.expand_path(ENV["CFNVPN_PATH"] || "~/.cfnvpn")
13
+ end
14
+
15
+ end
16
+ 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
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,
@@ -0,0 +1,29 @@
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 colorize(color_code)
15
+ "\e[#{color_code}m#{self}\e[0m"
16
+ end
17
+
18
+ def red
19
+ colorize(31)
20
+ end
21
+
22
+ def green
23
+ colorize(32)
24
+ end
25
+
26
+ def yellow
27
+ colorize(33)
28
+ end
29
+ 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,353 @@
1
+ require 'cfndsl'
2
+ require 'cfnvpn/templates/helper'
3
+
4
+ module CfnVpn
5
+ module Templates
6
+ class Vpn < CfnDsl::CloudFormationTemplate
7
+
8
+ def initialize
9
+ super
10
+ end
11
+
12
+ def render(name, config)
13
+ Description "cfnvpn #{name} AWS Client-VPN"
14
+ Parameter(:AssociateSubnets) {
15
+ Type 'String'
16
+ Default 'true'
17
+ AllowedValues ['true', 'false']
18
+ Description 'Toggle to false to disassociate all Client VPN subnet associations'
19
+ }
20
+
21
+ Condition(:EnableSubnetAssociation, FnEquals(Ref(:AssociateSubnets), 'true'))
22
+
23
+ Logs_LogGroup(:ClientVpnLogGroup) {
24
+ LogGroupName FnSub("#{name}-ClientVpn")
25
+ RetentionInDays 30
26
+ }
27
+
28
+ EC2_ClientVpnEndpoint(:ClientVpnEndpoint) {
29
+ Description FnSub("cfnvpn #{name} AWS Client-VPN")
30
+ AuthenticationOptions([
31
+ if config[:type] == 'federated'
32
+ {
33
+ FederatedAuthentication: {
34
+ SAMLProviderArn: config[:saml_arn],
35
+ SelfServiceSAMLProviderArn: config[:saml_arn]
36
+ },
37
+ Type: 'federated-authentication'
38
+ }
39
+ elsif config[:type] == 'active-directory'
40
+ {
41
+ ActiveDirectory: {
42
+ DirectoryId: config[:directory_id]
43
+ },
44
+ Type: 'directory-service-authentication'
45
+ }
46
+ else
47
+ {
48
+ MutualAuthentication: {
49
+ ClientRootCertificateChainArn: config[:client_cert_arn]
50
+ },
51
+ Type: 'certificate-authentication'
52
+ }
53
+ end
54
+ ])
55
+ ServerCertificateArn config[:server_cert_arn]
56
+ ClientCidrBlock config[:cidr]
57
+ ConnectionLogOptions({
58
+ CloudwatchLogGroup: Ref(:ClientVpnLogGroup),
59
+ Enabled: true
60
+ })
61
+ DnsServers config[:dns_servers].any? ? config[:dns_servers] : Ref('AWS::NoValue')
62
+ TagSpecifications([{
63
+ ResourceType: "client-vpn-endpoint",
64
+ Tags: [
65
+ { Key: 'Name', Value: name }
66
+ ]
67
+ }])
68
+ TransportProtocol config[:protocol]
69
+ SplitTunnel config[:split_tunnel]
70
+ }
71
+
72
+ config[:subnet_ids].each_with_index do |subnet, index|
73
+ suffix = index == 0 ? "" : "For#{subnet.resource_safe}"
74
+
75
+ EC2_ClientVpnTargetNetworkAssociation(:"ClientVpnTargetNetworkAssociation#{suffix}") {
76
+ Condition(:EnableSubnetAssociation)
77
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
78
+ SubnetId subnet
79
+ }
80
+
81
+ if config[:default_groups].any?
82
+ config[:default_groups].each do |group|
83
+ EC2_ClientVpnAuthorizationRule(:"TargetNetworkAuthorizationRule#{suffix}#{group.resource_safe}"[0..255]) {
84
+ Condition(:EnableSubnetAssociation)
85
+ DependsOn "ClientVpnTargetNetworkAssociation#{suffix}"
86
+ Description FnSub("#{name} client-vpn auth rule for subnet association")
87
+ AccessGroupId group
88
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
89
+ TargetNetworkCidr CfnVpn::Templates::Helper.get_auth_cidr(config[:region], subnet)
90
+ }
91
+ end
92
+ else
93
+ EC2_ClientVpnAuthorizationRule(:"TargetNetworkAuthorizationRule#{suffix}") {
94
+ Condition(:EnableSubnetAssociation)
95
+ DependsOn "ClientVpnTargetNetworkAssociation#{suffix}"
96
+ Description FnSub("#{name} client-vpn auth rule for subnet association")
97
+ AuthorizeAllGroups true
98
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
99
+ TargetNetworkCidr CfnVpn::Templates::Helper.get_auth_cidr(config[:region], subnet)
100
+ }
101
+ end
102
+
103
+ if subnet == config[:internet_route]
104
+ EC2_ClientVpnRoute(:RouteToInternet) {
105
+ Condition(:EnableSubnetAssociation)
106
+ DependsOn "ClientVpnTargetNetworkAssociation#{suffix}"
107
+ Description 'Route to the internet'
108
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
109
+ DestinationCidrBlock '0.0.0.0/0'
110
+ TargetVpcSubnetId config[:internet_route]
111
+ }
112
+
113
+ EC2_ClientVpnAuthorizationRule(:RouteToInternetAuthorizationRule) {
114
+ Condition(:EnableSubnetAssociation)
115
+ DependsOn "ClientVpnTargetNetworkAssociation#{suffix}"
116
+ Description 'Route to the internet'
117
+ AuthorizeAllGroups true
118
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
119
+ TargetNetworkCidr '0.0.0.0/0'
120
+ }
121
+
122
+ output(:InternetRoute, config[:internet_route])
123
+ end
124
+ end
125
+
126
+ config[:routes].each do |route|
127
+ EC2_ClientVpnRoute(:"#{route[:cidr].resource_safe}VpnRoute") {
128
+ Description route[:desc]
129
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
130
+ DestinationCidrBlock route[:cidr]
131
+ TargetVpcSubnetId route[:subnet]
132
+ }
133
+ if route[:groups].any?
134
+ route[:groups].each do |group|
135
+ EC2_ClientVpnAuthorizationRule(:"#{route[:cidr].resource_safe}AuthorizationRule#{group.resource_safe}"[0..255]) {
136
+ Description route[:desc]
137
+ AccessGroupId group
138
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
139
+ TargetNetworkCidr route[:cidr]
140
+ }
141
+ end
142
+ else
143
+ EC2_ClientVpnAuthorizationRule(:"#{route[:cidr].resource_safe}AllowAllAuthorizationRule") {
144
+ Description route[:desc]
145
+ AuthorizeAllGroups true
146
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
147
+ TargetNetworkCidr route[:cidr]
148
+ }
149
+ end
150
+ end
151
+
152
+ SSM_Parameter(:CfnVpnConfig) {
153
+ Description "#{name} cfnvpn config"
154
+ Name "/cfnvpn/config/#{name}"
155
+ Tier 'Standard'
156
+ Type 'String'
157
+ Value config.to_json
158
+ Tags({
159
+ Name: "#{name}-cfnvpn-config",
160
+ Environment: 'cfnvpn'
161
+ })
162
+ }
163
+
164
+ if config[:start] || config[:stop]
165
+ scheduler(name, config[:start], config[:stop])
166
+ output(:Start, config[:start]) if config[:start]
167
+ output(:Stop, config[:stop]) if config[:stop]
168
+ end
169
+
170
+ output(:ServerCertArn, config[:server_cert_arn])
171
+ output(:Cidr, config[:cidr])
172
+ output(:DnsServers, config.fetch(:dns_servers, []).join(','))
173
+ output(:SubnetIds, config[:subnet_ids].join(','))
174
+ output(:SplitTunnel, config[:split_tunnel])
175
+ output(:Protocol, config[:protocol])
176
+ output(:Type, config[:type])
177
+
178
+ if config[:type] == 'federated'
179
+ output(:SamlArn, config[:saml_arn])
180
+ elsif config[:type] == 'active-directory'
181
+ output(:DirectoryId, config[:directory_id])
182
+ else
183
+ output(:ClientCertArn, config[:client_cert_arn])
184
+ end
185
+ end
186
+
187
+ def output(name, value)
188
+ Output(name) { Value value }
189
+ end
190
+
191
+ def scheduler(name, start, stop)
192
+ IAM_Role(:ClientVpnSchedulerRole) {
193
+ AssumeRolePolicyDocument({
194
+ Version: '2012-10-17',
195
+ Statement: [{
196
+ Effect: 'Allow',
197
+ Principal: { Service: [ 'lambda.amazonaws.com' ] },
198
+ Action: [ 'sts:AssumeRole' ]
199
+ }]
200
+ })
201
+ Path '/cfnvpn/'
202
+ Policies([
203
+ {
204
+ PolicyName: 'cloudformation',
205
+ PolicyDocument: {
206
+ Version: '2012-10-17',
207
+ Statement: [{
208
+ Effect: 'Allow',
209
+ Action: [
210
+ 'cloudformation:UpdateStack'
211
+ ],
212
+ Resource: FnSub("arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/#{name}-cfnvpn/*")
213
+ }]
214
+ }
215
+ },
216
+ {
217
+ PolicyName: 'client-vpn',
218
+ PolicyDocument: {
219
+ Version: '2012-10-17',
220
+ Statement: [{
221
+ Effect: 'Allow',
222
+ Action: [
223
+ 'ec2:AssociateClientVpnTargetNetwork',
224
+ 'ec2:DisassociateClientVpnTargetNetwork',
225
+ 'ec2:DescribeClientVpnTargetNetworks',
226
+ 'ec2:AuthorizeClientVpnIngress',
227
+ 'ec2:RevokeClientVpnIngress',
228
+ 'ec2:DescribeClientVpnAuthorizationRules',
229
+ 'ec2:DescribeClientVpnEndpoints',
230
+ 'ec2:DescribeClientVpnConnections',
231
+ 'ec2:TerminateClientVpnConnections'
232
+ ],
233
+ Resource: '*'
234
+ }]
235
+ }
236
+ },
237
+ {
238
+ PolicyName: 'logging',
239
+ PolicyDocument: {
240
+ Version: '2012-10-17',
241
+ Statement: [{
242
+ Effect: 'Allow',
243
+ Action: [
244
+ 'logs:DescribeLogGroups',
245
+ 'logs:CreateLogGroup',
246
+ 'logs:CreateLogStream',
247
+ 'logs:DescribeLogStreams',
248
+ 'logs:PutLogEvents'
249
+ ],
250
+ Resource: '*'
251
+ }]
252
+ }
253
+ }
254
+ ])
255
+ Tags([
256
+ { Key: 'Name', Value: "#{name}-cfnvpn-scheduler-role" },
257
+ { Key: 'Environment', Value: 'cfnvpn' }
258
+ ])
259
+ }
260
+
261
+ Lambda_Function(:ClientVpnSchedulerFunction) {
262
+ Runtime 'python3.7'
263
+ Role FnGetAtt(:ClientVpnSchedulerRole, :Arn)
264
+ MemorySize '128'
265
+ Handler 'index.handler'
266
+ Code({
267
+ ZipFile: <<~EOS
268
+ import boto3
269
+
270
+ def handler(event, context):
271
+
272
+ print(f"updating cfn-vpn stack {event['StackName']} parameter AssociateSubnets with value {event['AssociateSubnets']}")
273
+
274
+ if event['AssociateSubnets'] == 'false':
275
+ print(f"terminating current vpn sessions to {event['ClientVpnEndpointId']}")
276
+ ec2 = boto3.client('ec2')
277
+ resp = ec2.describe_client_vpn_connections(ClientVpnEndpointId=event['ClientVpnEndpointId'])
278
+ for conn in resp['Connections']:
279
+ if conn['Status']['Code'] == 'active':
280
+ ec2.terminate_client_vpn_connections(
281
+ ClientVpnEndpointId=event['ClientVpnEndpointId'],
282
+ ConnectionId=conn['ConnectionId']
283
+ )
284
+ print(f"terminated session {conn['ConnectionId']}")
285
+
286
+ client = boto3.client('cloudformation')
287
+ print(client.update_stack(
288
+ StackName=event['StackName'],
289
+ UsePreviousTemplate=True,
290
+ Capabilities=['CAPABILITY_IAM'],
291
+ Parameters=[
292
+ {
293
+ 'ParameterKey': 'AssociateSubnets',
294
+ 'ParameterValue': event['AssociateSubnets']
295
+ }
296
+ ]
297
+ ))
298
+
299
+ return 'OK'
300
+ EOS
301
+ })
302
+ Tags([
303
+ { Key: 'Name', Value: "#{name}-cfnvpn-scheduler-function" },
304
+ { Key: 'Environment', Value: 'cfnvpn' }
305
+ ])
306
+ }
307
+
308
+ Logs_LogGroup(:ClientVpnSchedulerLogGroup) {
309
+ LogGroupName FnSub("/aws/lambda/${ClientVpnSchedulerFunction}")
310
+ RetentionInDays 30
311
+ }
312
+
313
+ Lambda_Permission(:ClientVpnSchedulerFunctionPermissions) {
314
+ FunctionName Ref(:ClientVpnSchedulerFunction)
315
+ Action 'lambda:InvokeFunction'
316
+ Principal 'events.amazonaws.com'
317
+ }
318
+
319
+ if start
320
+ Events_Rule(:ClientVpnSchedulerStart) {
321
+ State 'ENABLED'
322
+ Description "cfnvpn start schedule"
323
+ ScheduleExpression "cron(#{start})"
324
+ Targets([
325
+ {
326
+ Arn: FnGetAtt(:ClientVpnSchedulerFunction, :Arn),
327
+ Id: 'cfnvpnschedulerstart',
328
+ Input: FnSub({ StackName: "#{name}-cfnvpn", AssociateSubnets: 'true', ClientVpnEndpointId: "${ClientVpnEndpoint}" }.to_json)
329
+ }
330
+ ])
331
+ }
332
+ end
333
+
334
+ if stop
335
+ Events_Rule(:ClientVpnSchedulerStop) {
336
+ State 'ENABLED'
337
+ Description "cfnvpn stop schedule"
338
+ ScheduleExpression "cron(#{stop})"
339
+ Targets([
340
+ {
341
+ Arn: FnGetAtt(:ClientVpnSchedulerFunction, :Arn),
342
+ Id: 'cfnvpnschedulerstop',
343
+ Input: FnSub({ StackName: "#{name}-cfnvpn", AssociateSubnets: 'false', ClientVpnEndpointId: "${ClientVpnEndpoint}" }.to_json)
344
+ }
345
+ ])
346
+ }
347
+ end
348
+
349
+ end
350
+
351
+ end
352
+ end
353
+ end