cfn-vpn 0.4.2 → 1.2.0

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