LPX-673: compulsory env WAF on the shared ALB (+ dashboard panels)#103
Merged
stevethomas merged 11 commits intoJun 9, 2026
Conversation
…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>
waf: true — managed WAF on the shared ALBPHP/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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (
Waffacade +yolo waf:ban). Related: LPX-541.What problems are you solving?
sync:environmentwherever there's a load balancer, reconciled on every sync, no manifest key. (It started as awaf: truetoggle; 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 onExecutesWebStep— skipped only when headless.syncreconciles 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_BODY8 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:
WafV2wrapper + client + tag sync;WebAcl(SynchronisesConfiguration) +Allow/BlockIP set resources; 4Sync/Environmentsteps;'WafV2' => 'wafv2'audit catalogue entry.Is there anything the reviewer needs to know to deploy this?
sync:environmentper 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.IpSetis deliberately not aSynchronisesConfiguration(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 orUpdateWebACLfails on a duplicate priority.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).ResourceGroupsTaggingApiTestused awafv2ARN as its example of an unmanaged service; now thatwafv2is catalogued, the example moved toglobalaccelerator. Same assertion, different stand-in.🤖 Generated with Claude Code