Add shared CloudFront WAF WebACL (audit Day 3: M-17) (#7)

seahaven-app-waf (CLOUDFRONT scope, us-east-1): AWS managed Common + Known Bad
Inputs rule groups + per-IP rate limit (2000/5min). ARN published to SSM
/seahaven/waf/app-web-acl-arn for app stacks (meal-order/orders) to consume.
seahaven.com already has its own WAF; ledgerflow is being decommissioned
(INFRA-26); proposal-system-web skipped (not live).
This commit is contained in:
Adam Moussa 2026-06-02 16:42:24 -04:00 • committed by GitHub
parent 3ba90ddc40
commit 60e8b0e9ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 0 deletions

View file

@ -10,6 +10,7 @@ import { GovernanceToggles } from "./governance-toggles";
import { CisMonitoring } from "./cis-monitoring";
import { FlowLogs } from "./flow-logs";
import { SesMonitoring } from "./ses-monitoring";
import { AppWebAcl } from "./web-acl";
/**
* Account-level security baseline for Sea Haven (account 328440206208).
@ -154,6 +155,8 @@ export class AccountBaselineStack extends cdk.Stack {
});
new FlowLogs(this, "FlowLogs");
new SesMonitoring(this, "SesMonitoring");
// Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks.
new AppWebAcl(this, "AppWebAcl");
cdk.Tags.of(this).add("Project", "account-baseline");
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");

76
lib/web-acl.ts Normal file
View file

@ -0,0 +1,76 @@
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 class AppWebAcl extends Construct {
constructor(scope: Construct, id: string) {
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"),
},
],
});
new ssm.StringParameter(this, "AppWebAclArnParam", {
parameterName: "/seahaven/waf/app-web-acl-arn",
stringValue: webAcl.attrArn,
description: "ARN of the shared CloudFront WAF WebACL (audit M-17)",
});
new cdk.CfnOutput(this, "AppWebAclArn", { value: webAcl.attrArn });
}
}