seahaven-org-baseline/lib/web-acl.ts

106 lines
3.6 KiB
TypeScript
Raw Normal View History

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 });
}
}