@alliander-opensource/aws-jwt-sts 0.2.6
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.
- package/LICENSE.txt +201 -0
- package/README.md +130 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +451 -0
- package/dist/index.keyrotate.d.ts +1 -0
- package/dist/index.keyrotate.js +193 -0
- package/dist/index.sign.d.ts +2 -0
- package/dist/index.sign.js +120 -0
- package/dist/test/index.keyrotate.test.d.ts +1 -0
- package/dist/test/index.keyrotate.test.js +152 -0
- package/dist/test/index.sign.test.d.ts +1 -0
- package/dist/test/index.sign.test.js +146 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/test/index.test.js +62 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/index.keyrotate.ts +228 -0
- package/src/index.sign.ts +145 -0
- package/src/index.ts +597 -0
- package/src/test/index.keyrotate.test.ts +168 -0
- package/src/test/index.sign.test.ts +187 -0
- package/src/test/index.test.ts +72 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2023 Alliander NV
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
/* eslint-disable no-unused-vars */
|
|
6
|
+
import * as cdk from 'aws-cdk-lib'
|
|
7
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda'
|
|
8
|
+
import * as sfn from 'aws-cdk-lib/aws-stepfunctions'
|
|
9
|
+
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks'
|
|
10
|
+
import * as iam from 'aws-cdk-lib/aws-iam'
|
|
11
|
+
import { OrganizationPrincipal, PolicyDocument } from 'aws-cdk-lib/aws-iam'
|
|
12
|
+
import * as s3 from 'aws-cdk-lib/aws-s3'
|
|
13
|
+
import { BucketEncryption } from 'aws-cdk-lib/aws-s3'
|
|
14
|
+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
|
|
15
|
+
import * as cloudfrontOrigins from 'aws-cdk-lib/aws-cloudfront-origins'
|
|
16
|
+
import * as events from 'aws-cdk-lib/aws-events'
|
|
17
|
+
import * as targets from 'aws-cdk-lib/aws-events-targets'
|
|
18
|
+
import * as acm from 'aws-cdk-lib/aws-certificatemanager'
|
|
19
|
+
import * as route53 from 'aws-cdk-lib/aws-route53'
|
|
20
|
+
import * as route53targets from 'aws-cdk-lib/aws-route53-targets'
|
|
21
|
+
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
|
|
22
|
+
import { MethodLoggingLevel } from 'aws-cdk-lib/aws-apigateway'
|
|
23
|
+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2'
|
|
24
|
+
import * as sns from 'aws-cdk-lib/aws-sns'
|
|
25
|
+
import * as logs from 'aws-cdk-lib/aws-logs'
|
|
26
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'
|
|
27
|
+
import { TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch'
|
|
28
|
+
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'
|
|
29
|
+
import { Construct } from 'constructs'
|
|
30
|
+
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'
|
|
31
|
+
import { IHostedZone } from 'aws-cdk-lib/aws-route53'
|
|
32
|
+
|
|
33
|
+
export enum wafUsage {
|
|
34
|
+
ConstructProvided,
|
|
35
|
+
ProvideWebAclArn
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AwsJwtStsProps {
|
|
39
|
+
/**
|
|
40
|
+
* defaultAudience which is used in de JWT's
|
|
41
|
+
*/
|
|
42
|
+
readonly defaultAudience: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HostedZoneId of the domain used for hosting the sts function
|
|
46
|
+
*/
|
|
47
|
+
readonly hostedZoneId?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Name of the hostedZone.
|
|
51
|
+
*/
|
|
52
|
+
readonly hostedZoneName?: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional subdomain name of oidc discovery, default: oidc.
|
|
56
|
+
*/
|
|
57
|
+
readonly oidcSubdomain?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Optional subdomain name of the token api (on api gw), default: token.
|
|
61
|
+
*/
|
|
62
|
+
readonly tokenSubdomain?: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* If waf needs to be added to the API GW
|
|
66
|
+
*
|
|
67
|
+
* None: no waf is used
|
|
68
|
+
* ConstructProvided: the construct will deploy a wafAcl with opinionated rules
|
|
69
|
+
* ProvideWebAclArn: provide your own arn
|
|
70
|
+
*/
|
|
71
|
+
readonly apiGwWaf?: wafUsage;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Arn of the waf webAcl rule to be associated with the API GW
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
readonly apiGwWafWebAclArn?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The ID of the AWS Organization 0-xxxx
|
|
81
|
+
*
|
|
82
|
+
*/
|
|
83
|
+
readonly orgId?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CPU Architecture
|
|
87
|
+
*/
|
|
88
|
+
readonly architecture?: lambda.Architecture
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional boolean to specify if key rotation should be triggered on creation of the stack, default: false
|
|
92
|
+
*/
|
|
93
|
+
readonly disableKeyRotateOnCreate?: boolean
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optional custom name for the CloudWatch Alarm monitoring Step Function failures, default: sts-key_rotate_sfn-alarm
|
|
97
|
+
*/
|
|
98
|
+
readonly alarmNameKeyRotationStepFunctionFailed?: string
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Optional custom name for the CloudWatch Alarm monitoring 5xx errors on the API Gateway, default: sts-5xx_api_gw-alarm
|
|
102
|
+
*/
|
|
103
|
+
readonly alarmNameApiGateway5xx?: string
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Optional custom name for the CloudWatch Alarm monitoring Sign Lambda failures, default: sts-sign_errors_lambda-alarm
|
|
107
|
+
*/
|
|
108
|
+
readonly alarmNameSignLambdaFailed?: string
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Optional custom name for the CloudWatch Alarm monitoring Key Rotation Lambda failures, default: sts-key_rotate_errors_lambda-alarm
|
|
112
|
+
*/
|
|
113
|
+
readonly alarmNameKeyRotationLambdaFailed?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* eslint-disable no-new */
|
|
117
|
+
export class AwsJwtSts extends Construct {
|
|
118
|
+
/**
|
|
119
|
+
* SNS topic used to publish errors from the Step Function rotation flow
|
|
120
|
+
*/
|
|
121
|
+
public readonly failedRotationTopic: sns.Topic
|
|
122
|
+
|
|
123
|
+
constructor (app: Construct, id: string, props: AwsJwtStsProps) {
|
|
124
|
+
super(app, id)
|
|
125
|
+
|
|
126
|
+
/** ---------------------- Custom domain thingies ----------------------- */
|
|
127
|
+
|
|
128
|
+
let distributionDomainNames: string[] = []
|
|
129
|
+
let oidcCertificate: ICertificate | undefined
|
|
130
|
+
let tokenCertificate: ICertificate | undefined
|
|
131
|
+
let hostedZone: IHostedZone | undefined
|
|
132
|
+
const oidcSubdomain = props.oidcSubdomain ? props.oidcSubdomain : 'oidc'
|
|
133
|
+
const tokenSubdomain = props.tokenSubdomain ? props.tokenSubdomain : 'token'
|
|
134
|
+
const architecture = props.architecture ? props.architecture : lambda.Architecture.X86_64
|
|
135
|
+
let oidcDomainName = ''
|
|
136
|
+
let tokenDomainName = ''
|
|
137
|
+
|
|
138
|
+
const useCustomDomain = props.hostedZoneId && props.hostedZoneName
|
|
139
|
+
|
|
140
|
+
if (useCustomDomain) {
|
|
141
|
+
oidcDomainName = oidcSubdomain + '.' + props.hostedZoneName
|
|
142
|
+
tokenDomainName = tokenSubdomain + '.' + props.hostedZoneName
|
|
143
|
+
|
|
144
|
+
distributionDomainNames = [oidcDomainName]
|
|
145
|
+
|
|
146
|
+
hostedZone = route53.HostedZone.fromHostedZoneAttributes(
|
|
147
|
+
this,
|
|
148
|
+
'hostedZone',
|
|
149
|
+
{
|
|
150
|
+
zoneName: props.hostedZoneName!,
|
|
151
|
+
hostedZoneId: props.hostedZoneId!
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
oidcCertificate = new acm.DnsValidatedCertificate(this, 'CrossRegionCertificate', {
|
|
156
|
+
domainName: oidcDomainName,
|
|
157
|
+
hostedZone,
|
|
158
|
+
region: 'us-east-1'
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
tokenCertificate = new acm.Certificate(this, 'tokenCertificate', {
|
|
162
|
+
domainName: tokenDomainName,
|
|
163
|
+
validation: acm.CertificateValidation.fromDns(hostedZone)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** ---------------------- S3 Definition ----------------------- */
|
|
168
|
+
|
|
169
|
+
// Create bucket where oidc information can be stored
|
|
170
|
+
const oidcbucket = new s3.Bucket(this, 'oidcbucket', {
|
|
171
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
172
|
+
autoDeleteObjects: true,
|
|
173
|
+
encryption: BucketEncryption.S3_MANAGED,
|
|
174
|
+
versioned: true,
|
|
175
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/** ------------------- Cloudfront Definition ------------------- */
|
|
179
|
+
|
|
180
|
+
const cloudfrontOAI = new cloudfront.OriginAccessIdentity(this, 'cloudfront-OAI', {
|
|
181
|
+
comment: 'OAI for oidc'
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const distribution = new cloudfront.Distribution(this, 'oidcDistribution', {
|
|
185
|
+
domainNames: distributionDomainNames,
|
|
186
|
+
comment: 'Discovery endpoint for OIDC',
|
|
187
|
+
certificate: oidcCertificate,
|
|
188
|
+
defaultBehavior: {
|
|
189
|
+
origin: new cloudfrontOrigins.S3Origin(oidcbucket, { originAccessIdentity: cloudfrontOAI }),
|
|
190
|
+
compress: true,
|
|
191
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
192
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/** ------------------ Lambda Handlers Definition ------------------ */
|
|
197
|
+
|
|
198
|
+
const issuer = useCustomDomain ? 'https://' + oidcDomainName : 'https://' + distribution.distributionDomainName
|
|
199
|
+
|
|
200
|
+
const rotateKeysRole = new iam.Role(this, 'rotateKeysRole', {
|
|
201
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
202
|
+
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')]
|
|
203
|
+
})
|
|
204
|
+
const rotateKeys = new lambdaNodejs.NodejsFunction(this, 'keyrotate', {
|
|
205
|
+
timeout: cdk.Duration.seconds(5),
|
|
206
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
207
|
+
role: rotateKeysRole,
|
|
208
|
+
architecture,
|
|
209
|
+
environment: {
|
|
210
|
+
S3_BUCKET: oidcbucket.bucketName,
|
|
211
|
+
ISSUER: issuer
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const signRole = new iam.Role(this, 'signRole', {
|
|
216
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
217
|
+
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')]
|
|
218
|
+
})
|
|
219
|
+
const sign = new lambdaNodejs.NodejsFunction(this, 'sign', {
|
|
220
|
+
timeout: cdk.Duration.seconds(5),
|
|
221
|
+
runtime: lambda.Runtime.NODEJS_18_X,
|
|
222
|
+
role: signRole,
|
|
223
|
+
architecture,
|
|
224
|
+
environment: {
|
|
225
|
+
ISSUER: issuer,
|
|
226
|
+
DEFAULT_AUDIENCE: props.defaultAudience
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
/** ------------------------ SNS Topic ------------------------- */
|
|
231
|
+
|
|
232
|
+
this.failedRotationTopic = new sns.Topic(this, 'sts')
|
|
233
|
+
const snsFail = new tasks.SnsPublish(this, 'snsFailed', {
|
|
234
|
+
topic: this.failedRotationTopic,
|
|
235
|
+
subject: 'STS KeyRotate step function execution failed',
|
|
236
|
+
message: sfn.TaskInput.fromJsonPathAt('$')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
/** ------------------ Step functions Definition ------------------ */
|
|
240
|
+
|
|
241
|
+
const deletePreviousStep = new tasks.LambdaInvoke(this, 'delete Previous', {
|
|
242
|
+
lambdaFunction: rotateKeys,
|
|
243
|
+
payload: sfn.TaskInput.fromObject({
|
|
244
|
+
step: 'deletePrevious'
|
|
245
|
+
}),
|
|
246
|
+
outputPath: '$.Payload'
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const movePreviousStep = new tasks.LambdaInvoke(this, 'move Previous', {
|
|
250
|
+
lambdaFunction: rotateKeys,
|
|
251
|
+
payload: sfn.TaskInput.fromObject({
|
|
252
|
+
step: 'movePrevious'
|
|
253
|
+
}),
|
|
254
|
+
outputPath: '$.Payload'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const moveCurrentStep = new tasks.LambdaInvoke(this, 'move Current', {
|
|
258
|
+
lambdaFunction: rotateKeys,
|
|
259
|
+
payload: sfn.TaskInput.fromObject({
|
|
260
|
+
step: 'moveCurrent'
|
|
261
|
+
}),
|
|
262
|
+
outputPath: '$.Payload'
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const createPendingStep = new tasks.LambdaInvoke(this, 'create Pending', {
|
|
266
|
+
lambdaFunction: rotateKeys,
|
|
267
|
+
payload: sfn.TaskInput.fromObject({
|
|
268
|
+
step: 'createPending'
|
|
269
|
+
}),
|
|
270
|
+
outputPath: '$.Payload'
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const generateArtifactsStep = new tasks.LambdaInvoke(this, 'generate artifacts', {
|
|
274
|
+
lambdaFunction: rotateKeys,
|
|
275
|
+
payload: sfn.TaskInput.fromObject({
|
|
276
|
+
step: 'generateArtifacts'
|
|
277
|
+
}),
|
|
278
|
+
outputPath: '$.Payload'
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const jobFailed = new sfn.Fail(this, 'Failed', {
|
|
282
|
+
cause: 'AWS Batch Job Failed',
|
|
283
|
+
error: 'DescribeJob returned FAILED'
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const jobSuccess = new sfn.Succeed(this, 'Success!')
|
|
287
|
+
|
|
288
|
+
deletePreviousStep.addCatch(snsFail)
|
|
289
|
+
movePreviousStep.addCatch(snsFail)
|
|
290
|
+
moveCurrentStep.addCatch(snsFail)
|
|
291
|
+
createPendingStep.addCatch(snsFail)
|
|
292
|
+
generateArtifactsStep.addCatch(snsFail)
|
|
293
|
+
|
|
294
|
+
// Create chain
|
|
295
|
+
const definition = deletePreviousStep
|
|
296
|
+
.next(movePreviousStep)
|
|
297
|
+
.next(moveCurrentStep)
|
|
298
|
+
.next(createPendingStep)
|
|
299
|
+
.next(generateArtifactsStep)
|
|
300
|
+
.next(jobSuccess)
|
|
301
|
+
|
|
302
|
+
snsFail.next(jobFailed)
|
|
303
|
+
|
|
304
|
+
// Create state machine
|
|
305
|
+
const rotateKeysMachine = new sfn.StateMachine(this, 'RotateKeys', {
|
|
306
|
+
definition,
|
|
307
|
+
timeout: cdk.Duration.minutes(5)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
rotateKeys.grantInvoke(rotateKeysMachine.role)
|
|
311
|
+
oidcbucket.grantReadWrite(rotateKeys)
|
|
312
|
+
|
|
313
|
+
const statementSign = new iam.PolicyStatement()
|
|
314
|
+
statementSign.addActions('kms:*')
|
|
315
|
+
statementSign.addResources('*')
|
|
316
|
+
const signPolicy = new iam.ManagedPolicy(this, 'SignPolicy', {
|
|
317
|
+
statements: [statementSign]
|
|
318
|
+
})
|
|
319
|
+
signRole.addManagedPolicy(signPolicy)
|
|
320
|
+
|
|
321
|
+
const statementRotateKeys = new iam.PolicyStatement()
|
|
322
|
+
statementRotateKeys.addActions('kms:*')
|
|
323
|
+
statementRotateKeys.addResources('*')
|
|
324
|
+
const rotateKeysPolicy = new iam.ManagedPolicy(this, 'RotateKeysPolicy', {
|
|
325
|
+
statements: [statementRotateKeys]
|
|
326
|
+
})
|
|
327
|
+
rotateKeysRole.addManagedPolicy(rotateKeysPolicy)
|
|
328
|
+
|
|
329
|
+
/** ------------------ Events Rule Definition ------------------ */
|
|
330
|
+
|
|
331
|
+
// Run every 3 months at 8 PM UTC
|
|
332
|
+
const scheduledRotateRule = new events.Rule(this, 'scheduledRotateRule', {
|
|
333
|
+
schedule: events.Schedule.expression('cron(0 20 1 */3 ? *)')
|
|
334
|
+
})
|
|
335
|
+
scheduledRotateRule.addTarget(new targets.SfnStateMachine(rotateKeysMachine))
|
|
336
|
+
|
|
337
|
+
// Create state machine and trigger to populate initial keys
|
|
338
|
+
if (!props.disableKeyRotateOnCreate) {
|
|
339
|
+
const rotateOnce = new tasks.StepFunctionsStartExecution(this, 'rotateOnce', {
|
|
340
|
+
stateMachine: rotateKeysMachine,
|
|
341
|
+
integrationPattern: sfn.IntegrationPattern.RUN_JOB
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
const rotateTwice = new tasks.StepFunctionsStartExecution(this, 'rotateTwice', {
|
|
345
|
+
stateMachine: rotateKeysMachine,
|
|
346
|
+
integrationPattern: sfn.IntegrationPattern.RUN_JOB
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const populateKeys = new sfn.StateMachine(this, 'populateKeys', {
|
|
350
|
+
definition: rotateOnce.next(rotateTwice),
|
|
351
|
+
timeout: cdk.Duration.minutes(10)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const initialRunRule = new events.Rule(this, 'initialRunRule', {
|
|
355
|
+
eventPattern: {
|
|
356
|
+
source: ['aws.cloudformation'],
|
|
357
|
+
resources: [cdk.Stack.of(this).stackId],
|
|
358
|
+
detailType: ['CloudFormation Stack Status Change'],
|
|
359
|
+
detail: {
|
|
360
|
+
'status-details': {
|
|
361
|
+
status: ['CREATE_COMPLETE']
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
initialRunRule.addTarget(new targets.SfnStateMachine(populateKeys))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** ---------------------- API Gateway ----------------------- */
|
|
371
|
+
|
|
372
|
+
// only set policy when orgId is set
|
|
373
|
+
let apiPolicy: PolicyDocument | undefined
|
|
374
|
+
if (props.orgId) {
|
|
375
|
+
apiPolicy = new iam.PolicyDocument({
|
|
376
|
+
statements: [
|
|
377
|
+
new iam.PolicyStatement({
|
|
378
|
+
actions: ['execute-api:Invoke'],
|
|
379
|
+
resources: ['*'],
|
|
380
|
+
principals: [
|
|
381
|
+
new OrganizationPrincipal(props.orgId)
|
|
382
|
+
]
|
|
383
|
+
})
|
|
384
|
+
]
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const logGroup = new logs.LogGroup(this, 'APIGatewayAccessLogs', {
|
|
389
|
+
retention: 7
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Create API
|
|
393
|
+
const api = new apigateway.LambdaRestApi(this, 'jwk-sts-api', {
|
|
394
|
+
description: 'STS Token API Gateway',
|
|
395
|
+
handler: sign,
|
|
396
|
+
defaultMethodOptions: {
|
|
397
|
+
authorizationType: apigateway.AuthorizationType.IAM
|
|
398
|
+
},
|
|
399
|
+
endpointConfiguration: {
|
|
400
|
+
types: [apigateway.EndpointType.REGIONAL]
|
|
401
|
+
},
|
|
402
|
+
policy: apiPolicy,
|
|
403
|
+
deployOptions: {
|
|
404
|
+
loggingLevel: MethodLoggingLevel.INFO,
|
|
405
|
+
accessLogDestination: new apigateway.LogGroupLogDestination(logGroup)
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
/** ------------------- Route53 Definition for custom domain ------------------- */
|
|
410
|
+
|
|
411
|
+
if (useCustomDomain && hostedZone) {
|
|
412
|
+
api.addDomainName('apiCustomDomainName', {
|
|
413
|
+
domainName: tokenDomainName,
|
|
414
|
+
certificate: tokenCertificate!
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// Add A record for cloudfront distribution
|
|
418
|
+
|
|
419
|
+
new route53.ARecord(this, 'oidcRecord', {
|
|
420
|
+
recordName: oidcDomainName,
|
|
421
|
+
zone: hostedZone,
|
|
422
|
+
target: route53.RecordTarget.fromAlias(new route53targets.CloudFrontTarget(distribution))
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
new route53.ARecord(this, 'tokenRecord', {
|
|
426
|
+
recordName: tokenDomainName,
|
|
427
|
+
zone: hostedZone,
|
|
428
|
+
target: route53.RecordTarget.fromAlias(new route53targets.ApiGateway(api))
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
new cdk.CfnOutput(this, 'tokenEndpoint', {
|
|
432
|
+
value: 'https://' + tokenDomainName + '/token',
|
|
433
|
+
description: 'Url of the token endpoint',
|
|
434
|
+
exportName: 'tokenEndpoint'
|
|
435
|
+
})
|
|
436
|
+
} else {
|
|
437
|
+
new cdk.CfnOutput(this, 'tokenEndpoint', {
|
|
438
|
+
value: api.url + 'token',
|
|
439
|
+
description: 'Url of the token endpoint',
|
|
440
|
+
exportName: 'tokenEndpoint'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
new cdk.CfnOutput(this, 'issuer', {
|
|
445
|
+
value: issuer,
|
|
446
|
+
description: 'Url of the issuer',
|
|
447
|
+
exportName: 'issuer'
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
/** ---------------------- WAF ----------------------- */
|
|
451
|
+
|
|
452
|
+
if (props.apiGwWaf === wafUsage.ConstructProvided) {
|
|
453
|
+
// API gateway WAF ACL and rules
|
|
454
|
+
const APIGatewayWebACL = new wafv2.CfnWebACL(this, 'APIGatewayWebACL', {
|
|
455
|
+
description: 'This is WebACL for Auth APi Gateway',
|
|
456
|
+
scope: 'REGIONAL',
|
|
457
|
+
defaultAction: { allow: {} },
|
|
458
|
+
visibilityConfig: {
|
|
459
|
+
metricName: 'APIWebACL',
|
|
460
|
+
cloudWatchMetricsEnabled: true,
|
|
461
|
+
sampledRequestsEnabled: true
|
|
462
|
+
},
|
|
463
|
+
rules: [
|
|
464
|
+
{
|
|
465
|
+
name: 'AWS-AWSManagedRulesCommonRuleSet',
|
|
466
|
+
priority: 0,
|
|
467
|
+
statement: {
|
|
468
|
+
managedRuleGroupStatement: {
|
|
469
|
+
vendorName: 'AWS',
|
|
470
|
+
name: 'AWSManagedRulesCommonRuleSet'
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
overrideAction: {
|
|
474
|
+
none: {}
|
|
475
|
+
},
|
|
476
|
+
visibilityConfig: {
|
|
477
|
+
sampledRequestsEnabled: true,
|
|
478
|
+
cloudWatchMetricsEnabled: true,
|
|
479
|
+
metricName: 'AWS-AWSManagedRulesCommonRuleSet'
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: 'AWS-AWSManagedRulesAmazonIpReputationList',
|
|
484
|
+
priority: 1,
|
|
485
|
+
statement: {
|
|
486
|
+
managedRuleGroupStatement: {
|
|
487
|
+
vendorName: 'AWS',
|
|
488
|
+
name: 'AWSManagedRulesAmazonIpReputationList'
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
overrideAction: {
|
|
492
|
+
none: {}
|
|
493
|
+
},
|
|
494
|
+
visibilityConfig: {
|
|
495
|
+
sampledRequestsEnabled: true,
|
|
496
|
+
cloudWatchMetricsEnabled: true,
|
|
497
|
+
metricName: 'AWS-AWSManagedRulesAmazonIpReputationList'
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'api-gw-AuthAPIGeoLocation',
|
|
502
|
+
priority: 3,
|
|
503
|
+
action: { block: {} },
|
|
504
|
+
visibilityConfig: {
|
|
505
|
+
metricName: 'AuthAPIGeoLocation',
|
|
506
|
+
cloudWatchMetricsEnabled: true,
|
|
507
|
+
sampledRequestsEnabled: false
|
|
508
|
+
},
|
|
509
|
+
statement: {
|
|
510
|
+
geoMatchStatement: {
|
|
511
|
+
countryCodes: ['BY', 'CN', 'IR', 'RU', 'SY', 'KP']
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'api-gw-rateLimitRule',
|
|
517
|
+
priority: 4,
|
|
518
|
+
action: { block: {} },
|
|
519
|
+
visibilityConfig: {
|
|
520
|
+
metricName: 'rateLimitRule',
|
|
521
|
+
cloudWatchMetricsEnabled: true,
|
|
522
|
+
sampledRequestsEnabled: false
|
|
523
|
+
},
|
|
524
|
+
statement: {
|
|
525
|
+
rateBasedStatement: {
|
|
526
|
+
aggregateKeyType: 'IP',
|
|
527
|
+
limit: 100
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// Web ACL Association
|
|
535
|
+
new wafv2.CfnWebACLAssociation(this, 'APIGatewayWebACLAssociation', {
|
|
536
|
+
webAclArn: APIGatewayWebACL.attrArn,
|
|
537
|
+
resourceArn: api.deploymentStage.stageArn
|
|
538
|
+
})
|
|
539
|
+
} else if (props.apiGwWaf === wafUsage.ProvideWebAclArn && props.apiGwWafWebAclArn) {
|
|
540
|
+
// Web ACL Association
|
|
541
|
+
new wafv2.CfnWebACLAssociation(this, 'APIGatewayWebACLAssociation', {
|
|
542
|
+
webAclArn: props.apiGwWafWebAclArn,
|
|
543
|
+
resourceArn: api.deploymentStage.stageArn
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** ---------------------- Cloudwatch ----------------------- */
|
|
548
|
+
|
|
549
|
+
new cloudwatch.Alarm(this, 'StepFunctionError', {
|
|
550
|
+
alarmName: props.alarmNameKeyRotationStepFunctionFailed ?? 'sts-key_rotate_sfn-alarm',
|
|
551
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
552
|
+
threshold: 1,
|
|
553
|
+
evaluationPeriods: 1,
|
|
554
|
+
metric: rotateKeysMachine.metricFailed(),
|
|
555
|
+
alarmDescription: 'Key Rotation Failed',
|
|
556
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
new cloudwatch.Alarm(this, 'ApiGateway5XXAlarm', {
|
|
560
|
+
alarmName: props.alarmNameApiGateway5xx ?? 'sts-5xx_api_gw-alarm',
|
|
561
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
562
|
+
threshold: 1,
|
|
563
|
+
evaluationPeriods: 1,
|
|
564
|
+
metric: api.metricServerError(),
|
|
565
|
+
alarmDescription: '5xx STS API gateway failures',
|
|
566
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const signErrors = sign.metricErrors({
|
|
570
|
+
period: cdk.Duration.minutes(1)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const rotateErrors = rotateKeys.metricErrors({
|
|
574
|
+
period: cdk.Duration.minutes(1)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
new cloudwatch.Alarm(this, 'LambdaSignError', {
|
|
578
|
+
alarmName: props.alarmNameSignLambdaFailed ?? 'sts-sign_errors_lambda-alarm',
|
|
579
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
580
|
+
threshold: 1,
|
|
581
|
+
evaluationPeriods: 1,
|
|
582
|
+
metric: signErrors,
|
|
583
|
+
alarmDescription: 'Sign Lambda Failed',
|
|
584
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
new cloudwatch.Alarm(this, 'LambdaRotateError', {
|
|
588
|
+
alarmName: props.alarmNameKeyRotationLambdaFailed ?? 'sts-key_rotate_errors_lambda-alarm',
|
|
589
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
590
|
+
threshold: 1,
|
|
591
|
+
evaluationPeriods: 1,
|
|
592
|
+
metric: rotateErrors,
|
|
593
|
+
alarmDescription: 'Key Rotation Lambda Failed',
|
|
594
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
}
|