mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-03 02:13:13 +00:00
CloudFormation refreshes WebACL.Arn with GetWebACL when the parameter depends on it. The ACL is already gone, so that call fails the update before Retain can stick.
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import * as cdk from "aws-cdk-lib";
|
|
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
|
|
import * as ssm from "aws-cdk-lib/aws-ssm";
|
|
import { Construct } from "constructs";
|
|
|
|
/**
|
|
* Shared CloudFront WAF WebACL for Sea Haven app distributions (audit M-17).
|
|
*
|
|
* AWS managed rule groups (Common + Known Bad Inputs) plus an IP rate limit.
|
|
* CLOUDFRONT-scope WebACLs must live in us-east-1 — which is where this stack
|
|
* is — so it can be referenced by any app CloudFront distribution by ARN.
|
|
*
|
|
* The ARN is published to SSM (`/seahaven/waf/app-web-acl-arn`) so app stacks in
|
|
* other repos can consume it via `{{resolve:ssm:...}}` without a hard CFN export.
|
|
*/
|
|
export interface AppWebAclProps {
|
|
/**
|
|
* Deletion policy for the WebACL only. The SSM parameter keeps the default
|
|
* Delete policy so a later stack update can remove the parameter. Prod does
|
|
* not set this. Management sets RETAIN first because the live WebACL is
|
|
* already gone and a Delete call would fail and roll back into a recreate.
|
|
*/
|
|
readonly webAclRemovalPolicy?: cdk.RemovalPolicy;
|
|
/**
|
|
* Physical id of a WebACL CloudFormation already tracks. When set, the SSM
|
|
* parameter and output publish `global/webacl/seahaven-app-waf/<id>` with
|
|
* account and region tokens, and do not reference WebACL.Arn. GetAtt on a
|
|
* missing ACL calls GetWebACL and fails the stack update.
|
|
*/
|
|
readonly existingWebAclId?: string;
|
|
}
|
|
|
|
export class AppWebAcl extends Construct {
|
|
constructor(scope: Construct, id: string, props?: AppWebAclProps) {
|
|
super(scope, id);
|
|
|
|
const vis = (metric: string): wafv2.CfnWebACL.VisibilityConfigProperty => ({
|
|
cloudWatchMetricsEnabled: true,
|
|
sampledRequestsEnabled: true,
|
|
metricName: metric,
|
|
});
|
|
|
|
const webAcl = new wafv2.CfnWebACL(this, "AppWebAcl", {
|
|
name: "seahaven-app-waf",
|
|
scope: "CLOUDFRONT",
|
|
defaultAction: { allow: {} },
|
|
visibilityConfig: vis("seahaven-app-waf"),
|
|
rules: [
|
|
{
|
|
name: "AWSCommonRuleSet",
|
|
priority: 1,
|
|
overrideAction: { none: {} },
|
|
statement: {
|
|
managedRuleGroupStatement: {
|
|
vendorName: "AWS",
|
|
name: "AWSManagedRulesCommonRuleSet",
|
|
},
|
|
},
|
|
visibilityConfig: vis("AWSCommonRuleSet"),
|
|
},
|
|
{
|
|
name: "AWSKnownBadInputs",
|
|
priority: 2,
|
|
overrideAction: { none: {} },
|
|
statement: {
|
|
managedRuleGroupStatement: {
|
|
vendorName: "AWS",
|
|
name: "AWSManagedRulesKnownBadInputsRuleSet",
|
|
},
|
|
},
|
|
visibilityConfig: vis("AWSKnownBadInputs"),
|
|
},
|
|
{
|
|
name: "RateLimitPerIp",
|
|
priority: 3,
|
|
action: { block: {} },
|
|
statement: {
|
|
rateBasedStatement: { limit: 2000, aggregateKeyType: "IP" },
|
|
},
|
|
visibilityConfig: vis("RateLimitPerIp"),
|
|
},
|
|
],
|
|
});
|
|
if (props?.webAclRemovalPolicy) {
|
|
webAcl.applyRemovalPolicy(props.webAclRemovalPolicy);
|
|
}
|
|
|
|
const publishedArn = props?.existingWebAclId
|
|
? cdk.Stack.of(this).formatArn({
|
|
service: "wafv2",
|
|
resource: "global/webacl",
|
|
resourceName: `seahaven-app-waf/${props.existingWebAclId}`,
|
|
arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME,
|
|
})
|
|
: webAcl.attrArn;
|
|
|
|
new ssm.StringParameter(this, "AppWebAclArnParam", {
|
|
parameterName: "/seahaven/waf/app-web-acl-arn",
|
|
stringValue: publishedArn,
|
|
description: "ARN of the shared CloudFront WAF WebACL (audit M-17)",
|
|
});
|
|
|
|
new cdk.CfnOutput(this, "AppWebAclArn", { value: publishedArn });
|
|
}
|
|
}
|