cfn-vpn 0.4.1 → 1.1.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 (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 -224
  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 +99 -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 +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 -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 +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 -41
  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