/
client-vpn-endpoint.ts
451 lines (394 loc) · 13.2 KB
/
client-vpn-endpoint.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
import { Construct, DependencyGroup, IDependable } from 'constructs';
import { ClientVpnAuthorizationRule, ClientVpnAuthorizationRuleOptions } from './client-vpn-authorization-rule';
import { IClientVpnConnectionHandler, IClientVpnEndpoint, TransportProtocol, VpnPort } from './client-vpn-endpoint-types';
import { ClientVpnRoute, ClientVpnRouteOptions } from './client-vpn-route';
import { Connections } from './connections';
import { CfnClientVpnEndpoint, CfnClientVpnTargetNetworkAssociation } from './ec2.generated';
import { CidrBlock } from './network-util';
import { ISecurityGroup, SecurityGroup } from './security-group';
import { IVpc, SubnetSelection } from './vpc';
import { ISamlProvider } from '../../aws-iam';
import * as logs from '../../aws-logs';
import { CfnOutput, Resource, Token } from '../../core';
/**
* Options for a client VPN endpoint
*/
export interface ClientVpnEndpointOptions {
/**
* The IPv4 address range, in CIDR notation, from which to assign client IP
* addresses. The address range cannot overlap with the local CIDR of the VPC
* in which the associated subnet is located, or the routes that you add manually.
*
* Changing the address range will replace the Client VPN endpoint.
*
* The CIDR block should be /22 or greater.
*/
readonly cidr: string;
/**
* The ARN of the client certificate for mutual authentication.
*
* The certificate must be signed by a certificate authority (CA) and it must
* be provisioned in AWS Certificate Manager (ACM).
*
* @default - use user-based authentication
*/
readonly clientCertificateArn?: string;
/**
* The type of user-based authentication to use.
*
* @see https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/client-authentication.html
*
* @default - use mutual authentication
*/
readonly userBasedAuthentication?: ClientVpnUserBasedAuthentication;
/**
* Whether to enable connections logging
*
* @default true
*/
readonly logging?: boolean;
/**
* A CloudWatch Logs log group for connection logging
*
* @default - a new group is created
*/
readonly logGroup?: logs.ILogGroup;
/**
* A CloudWatch Logs log stream for connection logging
*
* @default - a new stream is created
*/
readonly logStream?: logs.ILogStream;
/**
* The AWS Lambda function used for connection authorization
*
* The name of the Lambda function must begin with the `AWSClientVPN-` prefix
*
* @default - no connection handler
*/
readonly clientConnectionHandler?: IClientVpnConnectionHandler;
/**
* A brief description of the Client VPN endpoint.
*
* @default - no description
*/
readonly description?: string;
/**
* The security groups to apply to the target network.
*
* @default - a new security group is created
*/
readonly securityGroups?: ISecurityGroup[];
/**
* Specify whether to enable the self-service portal for the Client VPN endpoint.
*
* @default true
*/
readonly selfServicePortal?: boolean;
/**
* The ARN of the server certificate
*/
readonly serverCertificateArn: string;
/**
* Indicates whether split-tunnel is enabled on the AWS Client VPN endpoint.
*
* @see https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/split-tunnel-vpn.html
*
* @default false
*/
readonly splitTunnel?: boolean;
/**
* The transport protocol to be used by the VPN session.
*
* @default TransportProtocol.UDP
*/
readonly transportProtocol?: TransportProtocol;
/**
* The port number to assign to the Client VPN endpoint for TCP and UDP
* traffic.
*
* @default VpnPort.HTTPS
*/
readonly port?: VpnPort;
/**
* Information about the DNS servers to be used for DNS resolution.
*
* A Client VPN endpoint can have up to two DNS servers.
*
* @default - use the DNS address configured on the device
*/
readonly dnsServers?: string[];
/**
* Subnets to associate to the client VPN endpoint.
*
* @default - the VPC default strategy
*/
readonly vpcSubnets?: SubnetSelection;
/**
* Whether to authorize all users to the VPC CIDR
*
* This automatically creates an authorization rule. Set this to `false` and
* use `addAuthorizationRule()` to create your own rules instead.
*
* @default true
*/
readonly authorizeAllUsersToVpcCidr?: boolean;
/**
* The maximum VPN session duration time.
*
* @default ClientVpnSessionTimeout.TWENTY_FOUR_HOURS
*/
readonly sessionTimeout?: ClientVpnSessionTimeout;
/**
* Customizable text that will be displayed in a banner on AWS provided clients
* when a VPN session is established.
*
* UTF-8 encoded characters only. Maximum of 1400 characters.
*
* @default - no banner is presented to the client
*/
readonly clientLoginBanner?: string;
}
/**
* Maximum VPN session duration time
*/
export enum ClientVpnSessionTimeout {
/** 8 hours */
EIGHT_HOURS = 8,
/** 10 hours */
TEN_HOURS = 10,
/** 12 hours */
TWELVE_HOURS = 12,
/** 24 hours */
TWENTY_FOUR_HOURS = 24,
}
/**
* User-based authentication for a client VPN endpoint
*/
export abstract class ClientVpnUserBasedAuthentication {
/**
* Active Directory authentication
*/
public static activeDirectory(directoryId: string): ClientVpnUserBasedAuthentication {
return new ActiveDirectoryAuthentication(directoryId);
}
/** Federated authentication */
public static federated(samlProvider: ISamlProvider, selfServiceSamlProvider?: ISamlProvider): ClientVpnUserBasedAuthentication {
return new FederatedAuthentication(samlProvider, selfServiceSamlProvider);
}
/** Renders the user based authentication */
public abstract render(): any;
}
/**
* Active Directory authentication
*/
class ActiveDirectoryAuthentication extends ClientVpnUserBasedAuthentication {
constructor(private readonly directoryId: string) {
super();
}
render(): any {
return {
type: 'directory-service-authentication',
activeDirectory: { directoryId: this.directoryId },
};
}
}
/**
* Federated authentication
*/
class FederatedAuthentication extends ClientVpnUserBasedAuthentication {
constructor(private readonly samlProvider: ISamlProvider, private readonly selfServiceSamlProvider?: ISamlProvider) {
super();
}
render(): any {
return {
type: 'federated-authentication',
federatedAuthentication: {
samlProviderArn: this.samlProvider.samlProviderArn,
selfServiceSamlProviderArn: this.selfServiceSamlProvider?.samlProviderArn,
},
};
}
}
/**
* Properties for a client VPN endpoint
*/
export interface ClientVpnEndpointProps extends ClientVpnEndpointOptions {
/**
* The VPC to connect to.
*/
readonly vpc: IVpc;
}
/**
* Attributes when importing an existing client VPN endpoint
*/
export interface ClientVpnEndpointAttributes {
/**
* The endpoint ID
*/
readonly endpointId: string;
/**
* The security groups associated with the endpoint
*/
readonly securityGroups: ISecurityGroup[];
}
/**
* A client VPN connnection
*/
export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint {
/**
* Import an existing client VPN endpoint
*/
public static fromEndpointAttributes(scope: Construct, id: string, attrs: ClientVpnEndpointAttributes): IClientVpnEndpoint {
class Import extends Resource implements IClientVpnEndpoint {
public readonly endpointId = attrs.endpointId;
public readonly connections = new Connections({ securityGroups: attrs.securityGroups });
public readonly targetNetworksAssociated: IDependable = new DependencyGroup();
}
return new Import(scope, id);
}
public readonly endpointId: string;
/**
* Allows specify security group connections for the endpoint.
*/
public readonly connections: Connections;
public readonly targetNetworksAssociated: IDependable;
private readonly _targetNetworksAssociated = new DependencyGroup();
constructor(scope: Construct, id: string, props: ClientVpnEndpointProps) {
super(scope, id);
if (!Token.isUnresolved(props.vpc.vpcCidrBlock)) {
const clientCidr = new CidrBlock(props.cidr);
const vpcCidr = new CidrBlock(props.vpc.vpcCidrBlock);
if (vpcCidr.containsCidr(clientCidr)) {
throw new Error('The client CIDR cannot overlap with the local CIDR of the VPC');
}
}
if (props.dnsServers && props.dnsServers.length > 2) {
throw new Error('A client VPN endpoint can have up to two DNS servers');
}
if (props.logging == false && (props.logGroup || props.logStream)) {
throw new Error('Cannot specify `logGroup` or `logStream` when logging is disabled');
}
if (props.clientConnectionHandler
&& !Token.isUnresolved(props.clientConnectionHandler.functionName)
&& !props.clientConnectionHandler.functionName.startsWith('AWSClientVPN-')) {
throw new Error('The name of the Lambda function must begin with the `AWSClientVPN-` prefix');
}
if (props.clientLoginBanner
&& !Token.isUnresolved(props.clientLoginBanner)
&& props.clientLoginBanner.length > 1400) {
throw new Error(`The maximum length for the client login banner is 1400, got ${props.clientLoginBanner.length}`);
}
const logging = props.logging ?? true;
const logGroup = logging
? props.logGroup ?? new logs.LogGroup(this, 'LogGroup')
: undefined;
const securityGroups = props.securityGroups ?? [new SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
})];
this.connections = new Connections({ securityGroups });
const endpoint = new CfnClientVpnEndpoint(this, 'Resource', {
authenticationOptions: renderAuthenticationOptions(props.clientCertificateArn, props.userBasedAuthentication),
clientCidrBlock: props.cidr,
clientConnectOptions: props.clientConnectionHandler
? {
enabled: true,
lambdaFunctionArn: props.clientConnectionHandler.functionArn,
}
: undefined,
connectionLogOptions: {
enabled: logging,
cloudwatchLogGroup: logGroup?.logGroupName,
cloudwatchLogStream: props.logStream?.logStreamName,
},
description: props.description,
dnsServers: props.dnsServers,
securityGroupIds: securityGroups.map(s => s.securityGroupId),
selfServicePortal: booleanToEnabledDisabled(props.selfServicePortal),
serverCertificateArn: props.serverCertificateArn,
splitTunnel: props.splitTunnel,
transportProtocol: props.transportProtocol,
vpcId: props.vpc.vpcId,
vpnPort: props.port,
sessionTimeoutHours: props.sessionTimeout,
clientLoginBannerOptions: props.clientLoginBanner
? {
enabled: true,
bannerText: props.clientLoginBanner,
}
: undefined,
});
this.endpointId = endpoint.ref;
if (props.userBasedAuthentication && (props.selfServicePortal ?? true)) {
// Output self-service portal URL
new CfnOutput(this, 'SelfServicePortalUrl', {
value: `https://self-service.clientvpn.amazonaws.com/endpoints/${this.endpointId}`,
});
}
// Associate subnets
const subnetIds = props.vpc.selectSubnets(props.vpcSubnets).subnetIds;
if (Token.isUnresolved(subnetIds)) {
throw new Error('Cannot associate subnets when VPC are imported from parameters or exports containing lists of subnet IDs.');
}
for (const [idx, subnetId] of Object.entries(subnetIds)) {
this._targetNetworksAssociated.add(new CfnClientVpnTargetNetworkAssociation(this, `Association${idx}`, {
clientVpnEndpointId: this.endpointId,
subnetId,
}));
}
this.targetNetworksAssociated = this._targetNetworksAssociated;
if (props.authorizeAllUsersToVpcCidr ?? true) {
this.addAuthorizationRule('AuthorizeAll', {
cidr: props.vpc.vpcCidrBlock,
});
}
}
/**
* Adds an authorization rule to this endpoint
*/
public addAuthorizationRule(id: string, props: ClientVpnAuthorizationRuleOptions): ClientVpnAuthorizationRule {
return new ClientVpnAuthorizationRule(this, id, {
...props,
clientVpnEndpoint: this,
});
}
/**
* Adds a route to this endpoint
*/
public addRoute(id: string, props: ClientVpnRouteOptions): ClientVpnRoute {
return new ClientVpnRoute(this, id, {
...props,
clientVpnEndpoint: this,
});
}
}
function renderAuthenticationOptions(
clientCertificateArn?: string,
userBasedAuthentication?: ClientVpnUserBasedAuthentication): CfnClientVpnEndpoint.ClientAuthenticationRequestProperty[] {
const authenticationOptions: CfnClientVpnEndpoint.ClientAuthenticationRequestProperty[] = [];
if (clientCertificateArn) {
authenticationOptions.push({
type: 'certificate-authentication',
mutualAuthentication: {
clientRootCertificateChainArn: clientCertificateArn,
},
});
}
if (userBasedAuthentication) {
authenticationOptions.push(userBasedAuthentication.render());
}
if (authenticationOptions.length === 0) {
throw new Error('A client VPN endpoint must use at least one authentication option');
}
return authenticationOptions;
}
function booleanToEnabledDisabled(val?: boolean): 'enabled' | 'disabled' | undefined {
switch (val) {
case undefined:
return undefined;
case true:
return 'enabled';
case false:
return 'disabled';
}
}