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
@@ -0,0 +1,36 @@
1
+ import boto3
2
+ import logging
3
+
4
+ logger = logging.getLogger()
5
+ logger.setLevel(logging.INFO)
6
+
7
+ def handler(event, context):
8
+
9
+ logger.info(f"updating cfn-vpn stack {event['StackName']} parameter AssociateSubnets with value {event['AssociateSubnets']}")
10
+
11
+ if event['AssociateSubnets'] == 'false':
12
+ logger.info(f"terminating current vpn sessions to {event['ClientVpnEndpointId']}")
13
+ ec2 = boto3.client('ec2')
14
+ resp = ec2.describe_client_vpn_connections(ClientVpnEndpointId=event['ClientVpnEndpointId'])
15
+ for conn in resp['Connections']:
16
+ if conn['Status']['Code'] == 'active':
17
+ ec2.terminate_client_vpn_connections(
18
+ ClientVpnEndpointId=event['ClientVpnEndpointId'],
19
+ ConnectionId=conn['ConnectionId']
20
+ )
21
+ logger.info(f"terminated session {conn['ConnectionId']}")
22
+
23
+ client = boto3.client('cloudformation')
24
+ logger.info(client.update_stack(
25
+ StackName=event['StackName'],
26
+ UsePreviousTemplate=True,
27
+ Capabilities=['CAPABILITY_IAM'],
28
+ Parameters=[
29
+ {
30
+ 'ParameterKey': 'AssociateSubnets',
31
+ 'ParameterValue': event['AssociateSubnets']
32
+ }
33
+ ]
34
+ ))
35
+
36
+ return 'OK'
@@ -0,0 +1,449 @@
1
+ require 'cfndsl'
2
+ require 'cfnvpn/templates/helper'
3
+ require 'cfnvpn/templates/lambdas'
4
+
5
+ module CfnVpn
6
+ module Templates
7
+ class Vpn < CfnDsl::CloudFormationTemplate
8
+
9
+ def initialize
10
+ super
11
+ end
12
+
13
+ def render(name, config)
14
+ Description "cfnvpn #{name} AWS Client-VPN"
15
+ Parameter(:AssociateSubnets) {
16
+ Type 'String'
17
+ Default 'true'
18
+ AllowedValues ['true', 'false']
19
+ Description 'Toggle to false to disassociate all Client VPN subnet associations'
20
+ }
21
+
22
+ Condition(:EnableSubnetAssociation, FnEquals(Ref(:AssociateSubnets), 'true'))
23
+
24
+ Logs_LogGroup(:ClientVpnLogGroup) {
25
+ LogGroupName FnSub("#{name}-ClientVpn")
26
+ RetentionInDays 30
27
+ }
28
+
29
+ EC2_ClientVpnEndpoint(:ClientVpnEndpoint) {
30
+ Description FnSub("cfnvpn #{name} AWS Client-VPN")
31
+ AuthenticationOptions([
32
+ if config[:type] == 'federated'
33
+ {
34
+ FederatedAuthentication: {
35
+ SAMLProviderArn: config[:saml_arn],
36
+ SelfServiceSAMLProviderArn: config[:saml_arn]
37
+ },
38
+ Type: 'federated-authentication'
39
+ }
40
+ elsif config[:type] == 'active-directory'
41
+ {
42
+ ActiveDirectory: {
43
+ DirectoryId: config[:directory_id]
44
+ },
45
+ Type: 'directory-service-authentication'
46
+ }
47
+ else
48
+ {
49
+ MutualAuthentication: {
50
+ ClientRootCertificateChainArn: config[:client_cert_arn]
51
+ },
52
+ Type: 'certificate-authentication'
53
+ }
54
+ end
55
+ ])
56
+ ServerCertificateArn config[:server_cert_arn]
57
+ ClientCidrBlock config[:cidr]
58
+ ConnectionLogOptions({
59
+ CloudwatchLogGroup: Ref(:ClientVpnLogGroup),
60
+ Enabled: true
61
+ })
62
+ DnsServers config[:dns_servers].any? ? config[:dns_servers] : Ref('AWS::NoValue')
63
+ TagSpecifications([{
64
+ ResourceType: "client-vpn-endpoint",
65
+ Tags: [
66
+ { Key: 'Name', Value: name }
67
+ ]
68
+ }])
69
+ TransportProtocol config[:protocol]
70
+ SplitTunnel config[:split_tunnel]
71
+ }
72
+
73
+ network_assoc_dependson = []
74
+ config[:subnet_ids].each_with_index do |subnet, index|
75
+ suffix = index == 0 ? "" : "For#{subnet.resource_safe}"
76
+
77
+ EC2_ClientVpnTargetNetworkAssociation(:"ClientVpnTargetNetworkAssociation#{suffix}") {
78
+ Condition(:EnableSubnetAssociation)
79
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
80
+ SubnetId subnet
81
+ }
82
+
83
+ network_assoc_dependson << "ClientVpnTargetNetworkAssociation#{suffix}"
84
+ end
85
+
86
+ if config[:default_groups].any?
87
+ config[:default_groups].each do |group|
88
+ EC2_ClientVpnAuthorizationRule(:"TargetNetworkAuthorizationRule#{group.resource_safe}"[0..255]) {
89
+ Condition(:EnableSubnetAssociation)
90
+ DependsOn network_assoc_dependson if network_assoc_dependson.any?
91
+ Description FnSub("#{name} client-vpn auth rule for subnet association")
92
+ AccessGroupId group
93
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
94
+ TargetNetworkCidr CfnVpn::Templates::Helper.get_auth_cidr(config[:region], config[:subnet_ids].first)
95
+ }
96
+ end
97
+ else
98
+ EC2_ClientVpnAuthorizationRule(:"TargetNetworkAuthorizationRule") {
99
+ Condition(:EnableSubnetAssociation)
100
+ DependsOn network_assoc_dependson if network_assoc_dependson.any?
101
+ Description FnSub("#{name} client-vpn auth rule for subnet association")
102
+ AuthorizeAllGroups true
103
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
104
+ TargetNetworkCidr CfnVpn::Templates::Helper.get_auth_cidr(config[:region], config[:subnet_ids].first)
105
+ }
106
+ end
107
+
108
+ if !config[:internet_route].nil?
109
+ EC2_ClientVpnRoute(:RouteToInternet) {
110
+ Condition(:EnableSubnetAssociation)
111
+ DependsOn network_assoc_dependson if network_assoc_dependson.any?
112
+ Description "Route to the internet through subnet #{config[:internet_route]}"
113
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
114
+ DestinationCidrBlock '0.0.0.0/0'
115
+ TargetVpcSubnetId config[:internet_route]
116
+ }
117
+
118
+ EC2_ClientVpnAuthorizationRule(:RouteToInternetAuthorizationRule) {
119
+ Condition(:EnableSubnetAssociation)
120
+ DependsOn network_assoc_dependson if network_assoc_dependson.any?
121
+ Description "Internet route authorization from subnet #{config[:internet_route]}"
122
+ AuthorizeAllGroups true
123
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
124
+ TargetNetworkCidr '0.0.0.0/0'
125
+ }
126
+
127
+ output(:InternetRoute, config[:internet_route])
128
+ end
129
+
130
+ dns_routes = config[:routes].select {|route| route.has_key?(:dns)}
131
+ cidr_routes = config[:routes].select {|route| route.has_key?(:cidr)}
132
+
133
+ if dns_routes.any?
134
+ auto_route_populator(name, config[:bucket])
135
+
136
+ dns_routes.each do |route|
137
+ input = {
138
+ Record: route[:dns],
139
+ ClientVpnEndpointId: "${ClientVpnEndpoint}",
140
+ TargetSubnet: route[:subnet],
141
+ Description: route[:desc]
142
+ }
143
+
144
+ if route[:groups].any?
145
+ input[:Groups] = route[:groups]
146
+ end
147
+
148
+ Events_Rule(:"CfnVpnAutoRoutePopulatorEvent#{route[:dns].resource_safe}"[0..255]) {
149
+ State 'ENABLED'
150
+ Description "cfnvpn auto route populator schedule for #{route[:dns]}"
151
+ ScheduleExpression "rate(5 minutes)"
152
+ Targets([
153
+ {
154
+ Arn: FnGetAtt(:CfnVpnAutoRoutePopulator, :Arn),
155
+ Id: "cfnvpnautoroutepopulator#{route[:dns].event_id_safe}",
156
+ Input: FnSub(input.to_json)
157
+ }
158
+ ])
159
+ }
160
+ end
161
+ end
162
+
163
+ if cidr_routes.any?
164
+ cidr_routes.each do |route|
165
+ EC2_ClientVpnRoute(:"#{route[:cidr].resource_safe}VpnRoute") {
166
+ Description "cfnvpn static route for #{route[:cidr]}. #{route[:desc]}".strip
167
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
168
+ DestinationCidrBlock route[:cidr]
169
+ TargetVpcSubnetId route[:subnet]
170
+ }
171
+
172
+ if route[:groups].any?
173
+ route[:groups].each do |group|
174
+ EC2_ClientVpnAuthorizationRule(:"#{route[:cidr].resource_safe}AuthorizationRule#{group.resource_safe}"[0..255]) {
175
+ Description "cfnvpn static authorization rule for group #{group} to route #{route[:cidr]}. #{route[:desc]}".strip
176
+ AccessGroupId group
177
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
178
+ TargetNetworkCidr route[:cidr]
179
+ }
180
+ end
181
+ else
182
+ EC2_ClientVpnAuthorizationRule(:"#{route[:cidr].resource_safe}AllowAllAuthorizationRule") {
183
+ Description "cfnvpn static allow all authorization rule to route #{route[:cidr]}. #{route[:desc]}".strip
184
+ AuthorizeAllGroups true
185
+ ClientVpnEndpointId Ref(:ClientVpnEndpoint)
186
+ TargetNetworkCidr route[:cidr]
187
+ }
188
+ end
189
+ end
190
+ end
191
+
192
+ SSM_Parameter(:CfnVpnConfig) {
193
+ Description "#{name} cfnvpn config"
194
+ Name "/cfnvpn/config/#{name}"
195
+ Tier 'Standard'
196
+ Type 'String'
197
+ Value config.to_json
198
+ Tags({
199
+ Name: "#{name}-cfnvpn-config",
200
+ Environment: 'cfnvpn'
201
+ })
202
+ }
203
+
204
+ if config[:start] || config[:stop]
205
+ scheduler(name, config[:start], config[:stop], config[:bucket])
206
+ output(:Start, config[:start]) if config[:start]
207
+ output(:Stop, config[:stop]) if config[:stop]
208
+ end
209
+
210
+ output(:ServerCertArn, config[:server_cert_arn])
211
+ output(:Cidr, config[:cidr])
212
+ output(:DnsServers, config.fetch(:dns_servers, []).join(','))
213
+ output(:SubnetIds, config[:subnet_ids].join(','))
214
+ output(:SplitTunnel, config[:split_tunnel])
215
+ output(:Protocol, config[:protocol])
216
+ output(:Type, config[:type])
217
+
218
+ if config[:type] == 'federated'
219
+ output(:SamlArn, config[:saml_arn])
220
+ elsif config[:type] == 'active-directory'
221
+ output(:DirectoryId, config[:directory_id])
222
+ else
223
+ output(:ClientCertArn, config[:client_cert_arn])
224
+ end
225
+ end
226
+
227
+ def output(name, value)
228
+ Output(name) { Value value }
229
+ end
230
+
231
+ def auto_route_populator(name, bucket)
232
+ IAM_Role(:CfnVpnAutoRoutePopulatorRole) {
233
+ AssumeRolePolicyDocument({
234
+ Version: '2012-10-17',
235
+ Statement: [{
236
+ Effect: 'Allow',
237
+ Principal: { Service: [ 'lambda.amazonaws.com' ] },
238
+ Action: [ 'sts:AssumeRole' ]
239
+ }]
240
+ })
241
+ Path '/cfnvpn/'
242
+ Policies([
243
+ {
244
+ PolicyName: 'client-vpn',
245
+ PolicyDocument: {
246
+ Version: '2012-10-17',
247
+ Statement: [{
248
+ Effect: 'Allow',
249
+ Action: [
250
+ 'ec2:AuthorizeClientVpnIngress',
251
+ 'ec2:RevokeClientVpnIngress',
252
+ 'ec2:DescribeClientVpnAuthorizationRules',
253
+ 'ec2:DescribeClientVpnEndpoints',
254
+ 'ec2:DescribeClientVpnRoutes',
255
+ 'ec2:CreateClientVpnRoute',
256
+ 'ec2:DeleteClientVpnRoute'
257
+ ],
258
+ Resource: '*'
259
+ }]
260
+ }
261
+ },
262
+ {
263
+ PolicyName: 'logging',
264
+ PolicyDocument: {
265
+ Version: '2012-10-17',
266
+ Statement: [{
267
+ Effect: 'Allow',
268
+ Action: [
269
+ 'logs:DescribeLogGroups',
270
+ 'logs:CreateLogGroup',
271
+ 'logs:CreateLogStream',
272
+ 'logs:DescribeLogStreams',
273
+ 'logs:PutLogEvents'
274
+ ],
275
+ Resource: '*'
276
+ }]
277
+ }
278
+ }
279
+ ])
280
+ Tags([
281
+ { Key: 'Name', Value: "#{name}-cfnvpn-auto-route-populator-role" },
282
+ { Key: 'Environment', Value: 'cfnvpn' }
283
+ ])
284
+ }
285
+
286
+ s3_key = CfnVpn::Templates::Lambdas.package_lambda(name: name, bucket: bucket, func: 'auto_route_populator', files: ['app.py'])
287
+
288
+ Lambda_Function(:CfnVpnAutoRoutePopulator) {
289
+ Runtime 'python3.8'
290
+ Role FnGetAtt(:CfnVpnAutoRoutePopulatorRole, :Arn)
291
+ MemorySize '128'
292
+ Handler 'app.handler'
293
+ Timeout 60
294
+ Code({
295
+ S3Bucket: bucket,
296
+ S3Key: s3_key
297
+ })
298
+ Tags([
299
+ { Key: 'Name', Value: "#{name}-cfnvpn-auto-route-populator" },
300
+ { Key: 'Environment', Value: 'cfnvpn' }
301
+ ])
302
+ }
303
+
304
+ Logs_LogGroup(:CfnVpnAutoRoutePopulatorLogGroup) {
305
+ LogGroupName FnSub("/aws/lambda/${CfnVpnAutoRoutePopulator}")
306
+ RetentionInDays 30
307
+ }
308
+
309
+ Lambda_Permission(:CfnVpnAutoRoutePopulatorFunctionPermissions) {
310
+ FunctionName Ref(:CfnVpnAutoRoutePopulator)
311
+ Action 'lambda:InvokeFunction'
312
+ Principal 'events.amazonaws.com'
313
+ }
314
+ end
315
+
316
+ def scheduler(name, start, stop, bucket)
317
+ IAM_Role(:ClientVpnSchedulerRole) {
318
+ AssumeRolePolicyDocument({
319
+ Version: '2012-10-17',
320
+ Statement: [{
321
+ Effect: 'Allow',
322
+ Principal: { Service: [ 'lambda.amazonaws.com' ] },
323
+ Action: [ 'sts:AssumeRole' ]
324
+ }]
325
+ })
326
+ Path '/cfnvpn/'
327
+ Policies([
328
+ {
329
+ PolicyName: 'cloudformation',
330
+ PolicyDocument: {
331
+ Version: '2012-10-17',
332
+ Statement: [{
333
+ Effect: 'Allow',
334
+ Action: [
335
+ 'cloudformation:UpdateStack'
336
+ ],
337
+ Resource: FnSub("arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/#{name}-cfnvpn/*")
338
+ }]
339
+ }
340
+ },
341
+ {
342
+ PolicyName: 'client-vpn',
343
+ PolicyDocument: {
344
+ Version: '2012-10-17',
345
+ Statement: [{
346
+ Effect: 'Allow',
347
+ Action: [
348
+ 'ec2:AssociateClientVpnTargetNetwork',
349
+ 'ec2:DisassociateClientVpnTargetNetwork',
350
+ 'ec2:DescribeClientVpnTargetNetworks',
351
+ 'ec2:AuthorizeClientVpnIngress',
352
+ 'ec2:RevokeClientVpnIngress',
353
+ 'ec2:DescribeClientVpnAuthorizationRules',
354
+ 'ec2:DescribeClientVpnEndpoints',
355
+ 'ec2:DescribeClientVpnConnections',
356
+ 'ec2:TerminateClientVpnConnections'
357
+ ],
358
+ Resource: '*'
359
+ }]
360
+ }
361
+ },
362
+ {
363
+ PolicyName: 'logging',
364
+ PolicyDocument: {
365
+ Version: '2012-10-17',
366
+ Statement: [{
367
+ Effect: 'Allow',
368
+ Action: [
369
+ 'logs:DescribeLogGroups',
370
+ 'logs:CreateLogGroup',
371
+ 'logs:CreateLogStream',
372
+ 'logs:DescribeLogStreams',
373
+ 'logs:PutLogEvents'
374
+ ],
375
+ Resource: '*'
376
+ }]
377
+ }
378
+ }
379
+ ])
380
+ Tags([
381
+ { Key: 'Name', Value: "#{name}-cfnvpn-scheduler-role" },
382
+ { Key: 'Environment', Value: 'cfnvpn' }
383
+ ])
384
+ }
385
+
386
+ s3_key = CfnVpn::Templates::Lambdas.package_lambda(name: name, bucket: bucket, func: 'scheduler', files: ['app.py'])
387
+
388
+ Lambda_Function(:ClientVpnSchedulerFunction) {
389
+ Runtime 'python3.8'
390
+ Role FnGetAtt(:ClientVpnSchedulerRole, :Arn)
391
+ MemorySize '128'
392
+ Handler 'app.handler'
393
+ Timeout 60
394
+ Code({
395
+ S3Bucket: bucket,
396
+ S3Key: s3_key
397
+ })
398
+ Tags([
399
+ { Key: 'Name', Value: "#{name}-cfnvpn-scheduler-function" },
400
+ { Key: 'Environment', Value: 'cfnvpn' }
401
+ ])
402
+ }
403
+
404
+ Logs_LogGroup(:ClientVpnSchedulerLogGroup) {
405
+ LogGroupName FnSub("/aws/lambda/${ClientVpnSchedulerFunction}")
406
+ RetentionInDays 30
407
+ }
408
+
409
+ Lambda_Permission(:ClientVpnSchedulerFunctionPermissions) {
410
+ FunctionName Ref(:ClientVpnSchedulerFunction)
411
+ Action 'lambda:InvokeFunction'
412
+ Principal 'events.amazonaws.com'
413
+ }
414
+
415
+ if start
416
+ Events_Rule(:ClientVpnSchedulerStart) {
417
+ State 'ENABLED'
418
+ Description "cfnvpn start schedule"
419
+ ScheduleExpression "cron(#{start})"
420
+ Targets([
421
+ {
422
+ Arn: FnGetAtt(:ClientVpnSchedulerFunction, :Arn),
423
+ Id: 'cfnvpnschedulerstart',
424
+ Input: FnSub({ StackName: "#{name}-cfnvpn", AssociateSubnets: 'true', ClientVpnEndpointId: "${ClientVpnEndpoint}" }.to_json)
425
+ }
426
+ ])
427
+ }
428
+ end
429
+
430
+ if stop
431
+ Events_Rule(:ClientVpnSchedulerStop) {
432
+ State 'ENABLED'
433
+ Description "cfnvpn stop schedule"
434
+ ScheduleExpression "cron(#{stop})"
435
+ Targets([
436
+ {
437
+ Arn: FnGetAtt(:ClientVpnSchedulerFunction, :Arn),
438
+ Id: 'cfnvpnschedulerstop',
439
+ Input: FnSub({ StackName: "#{name}-cfnvpn", AssociateSubnets: 'false', ClientVpnEndpointId: "${ClientVpnEndpoint}" }.to_json)
440
+ }
441
+ ])
442
+ }
443
+ end
444
+
445
+ end
446
+
447
+ end
448
+ end
449
+ end