wafPulumi stack (cfx-waf) owning the AWS WAF web ACLs the CloudFront distributions use, and the log group they write to. Design: DASHBOARD_WAF.md § 2.1 and § 5. This stack attaches an ACL to nothing: each distribution's own stack sets an ACL's ARN as its webAclId.
| Resource | prod | dev |
|---|---|---|
wafv2.WebAcl, scope CLOUDFRONT, shared |
cfx-cloudfront-prod |
cfx-cloudfront-dev |
wafv2.WebAcl, scope CLOUDFRONT, admin |
cfx-cloudfront-admin-prod |
cfx-cloudfront-admin-dev |
wafv2.IpSet, admin's allowlist |
cfx-admin-allowlist-prod |
cfx-admin-allowlist-dev |
cloudwatch.LogGroup, 365 days |
aws-waf-logs-cfx-cloudfront-prod |
aws-waf-logs-cfx-cloudfront-dev |
cloudwatch.LogResourcePolicy |
cfx-waf-log-delivery-prod |
cfx-waf-log-delivery-dev |
wafv2.WebAclLoggingConfiguration, one each |
on both ACLs | same |
All are in us-east-1, the only Region a CloudFront-scope web ACL exists in; the rest of the estate is us-west-2. Both ACLs log to the one group; each record names its ACL in webaclId.
On the shared ACL every rule counts and none blocks, and the default action is allow, so attaching it changes no response. pulumi/rules.ts is the list and test/webAcl.test.ts fails if a rule could block.
| Priority | Rule | Action |
|---|---|---|
| 0 | rate-limit-per-ip |
count, above 2,000 requests in 5 minutes per address |
| 1 | AWSManagedRulesAmazonIpReputationList |
group overridden to count |
| 2 | AWSManagedRulesAnonymousIpList |
group overridden to count |
| 3 | AWSManagedRulesKnownBadInputsRuleSet |
group overridden to count |
| 4 | AWSManagedRulesCommonRuleSet |
group overridden to count |
| 5 | AWSManagedRulesSQLiRuleSet |
group overridden to count |
HostingProviderIPList and NoUserAgent_HEADER, are per-rule count overrides here, so they keep counting when their group is later allowed to block.rules.ts and to the test that holds every rule to count.The admin dashboard has its own ACL, which makes WARP the only way in. Admin's hostnames are proxied to the WARP tunnel, where a Cloudflare Access app requires the Gateway posture check, and the tunnel's connector forwards to the distribution from its NAT EIP (packages/warp/admin_dashboard.tf).
| Priority | Rule | Action |
|---|---|---|
| 0 | off-allowlist |
block a viewer outside the egress VPC's three NAT EIPs (cfx-admin-allowlist-*) |
| 1–5 | the managed groups above | counting, as on the shared ACL |
packages/ui/src/lib/session-client.ts trust the cf-* headers a NAT EIP viewer carries, the operator's address and location from Cloudflare's edge. Never move it to count while that holds.Every request the ACL evaluates is logged. The cookie and authorization headers and the query string are redacted, for the reason the customer dashboard's access logs omit them: they carry sessions, search text and account ids. Sampled requests are off, because log redaction does not apply to them.
webAclArn through a StackReference it declares itself.aws.wafv2.getWebAclOutput({ name, scope: "CLOUDFRONT", region: "us-east-1" }), with the name from cloudfrontWebAclName (or adminWebAclName for admin) in src/webAclName.ts, outside pulumi/ so a change to it redeploys those apps. The name is therefore load-bearing: renaming the ACL replaces it, which fails while a distribution still holds it.Outputs: webAclArn, webAclName, adminWebAclArn, adminAclName, logGroupName.
cfx-role-devops, the role CI applies this stack with and the dashboards deploy as, holds wafv2:* in its policy and in its boundary in both workload accounts (packages/aft/account-customizations/workloads-{nonprod,prod}/terraform/iam.tf). A dashboard deploy needs it too: the lookup by name is wafv2:ListWebACLs.
CD does not order this stack ahead of the dashboards' SST jobs. That only matters when the ACL does not exist yet or is being replaced: apply this stack first, then deploy the dashboards.
bun nx run waf:pulumi-preview # dev
bun nx run waf:pulumi-preview:prod
aws wafv2 list-web-acls --scope CLOUDFRONT --region us-east-1
aws wafv2 get-logging-configuration --region us-east-1 --resource-arn <webAclArn>