Skip to content

LPX-673: compulsory env WAF on the shared ALB (+ dashboard panels)#103

Merged
stevethomas merged 11 commits into
mainfrom
steve/lpx-673-env-level-waf-true-provision-a-sensible-default-waf-on-the
Jun 9, 2026
Merged

LPX-673: compulsory env WAF on the shared ALB (+ dashboard panels)#103
stevethomas merged 11 commits into
mainfrom
steve/lpx-673-env-level-waf-true-provision-a-sensible-default-waf-on-the

Conversation

@stevethomas

@stevethomas stevethomas commented Jun 9, 2026

Copy link
Copy Markdown
Member

Hey, I made a thing! 🥳

Closes LPX-673. Follow-ups: LPX-674 (logging), LPX-675 (CloudFront-scope WAF), LPX-676 (align LP's legacy WAF on cutover), LPX-677 (Waf facade + yolo waf:ban). Related: LPX-541.

What problems are you solving?

  • Every environment's ALB now gets a regional WAFv2 web ACL — compulsory env infrastructure, like the ALB or VPC. Provisioned automatically by sync:environment wherever there's a load balancer, reconciled on every sync, no manifest key. (It started as a waf: true toggle; a per-app flag for an env-scope resource invented a competing-manifests problem and a can't-disable gap, both of which dissolve once WAF is just always-on infra.) Gated on ExecutesWebStep — skipped only when headless.
  • Draws the ownership seam so sync reconciles the policy skeleton but never stomps operator-managed content: the allow/block IP sets are create-only, the country block is seed-only, and any hand-added rule is preserved by name.

The ruleset (default action Allow, then): allow IP set → block IP set → high-risk-country block → AWS managed groups (IP reputation, known-bad-inputs, CRS, SQLi, PHP — all Block; CRS's SizeRestrictions_BODY 8 KB cap carved out to Count so large legit POSTs aren't blocked) → per-IP rate limit. Managed groups referenced unversioned so AWS signature/IP-rep updates roll in for free. ~1,290 WCU, under the 1,500 cap.

Also in this PR:

  • CloudWatch dashboard panels — request disposition + blocked-by-rule, resolved by a live WebACL lookup so they appear for any app behind the ALB.
  • WafV2 wrapper + client + tag sync; WebAcl (SynchronisesConfiguration) + Allow/Block IP set resources; 4 Sync/Environment steps; 'WafV2' => 'wafv2' audit catalogue entry.
  • Docs: a high-level "Web application firewall" section in the provisioning guide — deliberately no country list, thresholds or per-rule actions (so the public reference isn't an evasion map).

Is there anything the reviewer needs to know to deploy this?

  • Additive on first sync. Next sync:environment per env creates the WebACL + IP sets and associates the ALB — no mutation of existing shared infra, all behind sync's plan/confirm gate. Zero blast radius until then.
  • Operator content is never reverted. IpSet is deliberately not a SynchronisesConfiguration (contents create-only); the country block is seed-only (excluded from the reconciled set); hand-added rules are preserved by name. YOLO reserves rule priorities 0, 1, 2, 10–14, 20 — operator rules must avoid those or UpdateWebACL fails on a duplicate priority.
  • The 8 KB body cap is observe-not-enforce by default (SizeRestrictions_BODY → Count), so CRS-in-Block can't break a legit large POST out of the box. LP keeps Count unless an upload-logic review confirms everything large is direct-to-S3 (tracked in LPX-676).
  • A pre-existing audit test changed: ResourceGroupsTaggingApiTest used a wafv2 ARN as its example of an unmanaged service; now that wafv2 is catalogued, the example moved to globalaccelerator. Same assertion, different stand-in.
  • 709 tests green; PHPStan L5 / Rector / Pint clean; coverage gate runs on CI's 8.4 job.

🤖 Generated with Claude Code

stevethomas and others added 5 commits June 9, 2026 12:15
…ed ALB (LPX-673)

Front the environment load balancer with a YOLO-managed regional WAFv2 web ACL.
Env-scoped, so one ACL protects every app on the ALB; written by sync:environment,
off by default, purely additive once enabled.

YOLO owns the policy skeleton and reconciles it; the operator owns the list
contents, which sync never touches:

- Default action Allow, then allow/block IP-set rules, the AWS managed groups
  (IP reputation + known-bad-inputs Block; CRS + SQLi Count-first), and a
  ~2000/5min per-IP rate limit. Managed groups referenced unversioned so AWS
  signature/IP-rep updates roll in on their own.
- IP sets are create-only (seeded empty) — an operator's mid-incident console
  edits survive every sync. Hand-added rules (matched by name) are preserved
  through reconciling updates too; YOLO only rewrites the rules it owns.

New: src/Aws/WafV2.php wrapper + wafV2 client + synchroniseWafV2Tags; WebAcl
(SynchronisesConfiguration) + Allow/Block IpSet resources; 4 Sync/Environment
steps; ExecutesWafStep gate on Manifest::wafEnabled(); `waf` manifest key;
'WafV2' => 'wafv2' audit catalogue entry; manifest + commands docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under `pest --parallel` each test file runs in its own worker, so the WAF
builder fns that WafStepsTest borrowed from WebAclTest were undefined there.
Relocate them to tests/Pest.php (loaded by every worker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…` toggle

The WAF isn't an optional feature like `ivs` — it's compulsory env
infrastructure like the ALB or VPC. There's no app that benefits from having
no WAF, and an app with genuinely incompatible needs sits on its own load
balancer/account anyway. Making it a per-app manifest toggle invented a
competing-manifests conflict (whose `waf:` wins on a shared ALB?) to solve a
problem that doesn't exist.

So: no `waf:` key, no `wafEnabled()`, no `ExecutesWafStep` gate. The four WAF
steps now run as standard `sync:environment` work gated on `ExecutesWebStep`
(skip only when the env is headless — no ALB to protect), exactly like the ALB
steps beside them. Provisioned automatically for every environment with a load
balancer; reconciled on every sync; conflict-free.

Docs move from a manifest-key reference to a "Web application firewall" section
in the provisioning guide (it's a resource now, not config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two panels under a "# WAF" header: request disposition (allowed / blocked /
counted) and a blocked-/counted-by-rule breakdown — the latter doubling as the
promote-decision view, since the Count-mode managed groups (CRS, SQLi) surface
as "would block" before you flip them.

The WebACL is env-shared, so it's resolved by a live lookup (not this app's
manifest) and the section is omitted until the ACL exists — every app behind the
ALB gets the panels regardless of which sync created the WAF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stevethomas stevethomas changed the title LPX-673: env-level waf: true — managed WAF on the shared ALB LPX-673: compulsory env WAF on the shared ALB (+ dashboard panels) Jun 9, 2026
stevethomas and others added 6 commits June 9, 2026 16:08
PHP/Laravel-targeted managed group, Count-first like CRS/SQLi — an oversight in
the initial skeleton for a PHP-focused deployer. Surfaces on the dashboard's
counted-by-rule panel alongside CRS and SQLi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rate limit retuned to LP's proven DoS config: 200 requests per rolling
  1-minute window per source IP (was 2000/5min).
- New default country block (Block) seeded with a high-risk list
  (CN/GH/KP/LB/NG/RU/BD/NP/IQ/IR/CI/BO) as a starting point. It's seed-only —
  created once, then operator-owned, so re-scoping the countries survives every
  sync (the WebACL reconcile only touches YOLO-owned rules). Mirrors the
  create-only IP-set model for a rule whose content can't live in its own
  resource.
- Dashboard by-rule panel gains a Geo block series.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SQLi and PHP are targeted, low-false-positive managed groups — there's no reason
to observe them first, so they override to None (the group's own Block actions
apply). Only the broad Core Rule Set stays in Count, since it's the one whose
fresh signatures can plausibly false-positive on legit traffic. Dashboard moves
SQLi/PHP into the blocked-by-rule series; CRS remains the lone counted series.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BO isn't a recognised scraping/fraud/botnet origin — it was an LP-specific
empirical addition that doesn't generalise to a YOLO default. Keep it on LP via
the per-platform tuning, not the hardcoded baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…docs

- CRS now blocks (override None) like the other groups, with a RuleActionOverride
  dropping SizeRestrictions_BODY to Count: its 8 KB request-body cap would block
  legitimate large POSTs that don't go direct-to-S3, a universal false-positive
  we'd rather observe than enforce. Nothing is left in Count by default.
- Dashboard by-rule panel now charts every group as blocked.
- Docs: the WAF section no longer publishes the country list, rate threshold, or
  per-rule actions — high-level only, so the public reference isn't an evasion map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WebAcl and association steps already carry assertSyncStepReconciles; the IP
set steps had bespoke create/create-only tests. Add the shared reconciler
contract (tag drift is their only reconcile axis, since contents are create-only)
so every WAF sync step matches the codebase pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stevethomas stevethomas merged commit 30b1376 into main Jun 9, 2026
6 checks passed
@stevethomas stevethomas deleted the steve/lpx-673-env-level-waf-true-provision-a-sensible-default-waf-on-the branch June 9, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant