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