diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 4f8c4a4..0333d9e 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -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"); diff --git a/lib/web-acl.ts b/lib/web-acl.ts new file mode 100644 index 0000000..02979f3 --- /dev/null +++ b/lib/web-acl.ts @@ -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 }); + } +}