@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/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
+ }