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