Skip to content

fix(apply): stop normalization widening an exact-path DENY to the whole LB - #3

Merged
henleda merged 1 commit into
mainfrom
fix-normalize-path-widening
Jul 27, 2026
Merged

fix(apply): stop normalization widening an exact-path DENY to the whole LB#3
henleda merged 1 commit into
mainfrom
fix-normalize-path-widening

Conversation

@henleda

@henleda henleda commented Jul 27, 2026

Copy link
Copy Markdown
Owner

A band-aid meant to block one endpoint was denying every path on the load balancer.

Cause

normalize_service_policy_spec nested-merges each matcher against _RULE_DEFAULTS so a partial
value keeps its required sub-keys. For path, that also injected the default
prefix_values: ["/"] into any rule naming only exact_values / regex_values / suffix_values.
XC ORs the path value lists, so the injected catch-all matched everything.

12 of the 31 generated rules on disk are exact-only and were affected — /api/pay,
/api/login, /admin/users, /users/v1/register, and eight more.

Verified live on vpcopilot-lab

Snapshot + verified rollback, probe policy deleted after; baseline was / → 307, /api/pay → 405.

normalized DENY path /__widening-probe__ / /api/pay
before {"prefix_values": ["/"], "exact_values": ["/__widening-probe__"]} 403 403 403
after {"exact_values": ["/__widening-probe__"]} 403 307 405

Impact

Not an outage. The safety spine catches it — the legit probe is blocked too, so legit_ok is
false, validation fails and the policy rolls back. What it cost is effectiveness: an exact-path
band-aid could never pass validation on the first attempt, so it burned refine attempts recovering
or was reported as a failure when the policy was correct and normalization broke it.

The fix

The nested merge exists to supply required structural sub-keys, not a matching value. It now
drops only the inherited catch-all, and only when the rule named a path itself. A rule naming no
path still gets prefix_values: ["/"] — that is what makes generate's trailing allow-all work
against XC's default-deny.

Tests, written before the fix

Both directions, since this is on every apply path for all seven controls:

  • New behaviour: exact-only, regex-only, suffix-only rules lose the catch-all.
  • Pins that must not change: an explicit prefix_values; prefix + exact together; a rule with
    no path at all; an empty path object; the trailing allow-all; the structural sub-keys
    (transformers, invert_matcher); every other nested matcher and unrelated required default.

Plus an old-vs-new diff over all 15 service-policy artifacts on disk: 12 changed, 3 identical,
and every changed rule differed only in path, all other fields byte-identical.

ruff clean · 200 pass · coverage 73%.

🤖 Generated with Claude Code

…le LB

`normalize_service_policy_spec` nested-merges each matcher against `_RULE_DEFAULTS` so a partial
value keeps its required sub-keys. For `path` that also injected the default
`prefix_values: ["/"]` into any rule that named only `exact_values` / `regex_values` /
`suffix_values` — and XC ORs the value lists, so a surgical one-endpoint DENY denied every path
on the load balancer.

Verified live on vpcopilot-lab (snapshot + verified rollback, policy deleted after):

  before  normalized DENY path {"prefix_values": ["/"], "exact_values": ["/__widening-probe__"]}
          GET /__widening-probe__ -> 403   GET / -> 403   GET /api/pay -> 403
  after   normalized DENY path {"exact_values": ["/__widening-probe__"]}
          GET /__widening-probe__ -> 403   GET / -> 307   GET /api/pay -> 405   (both baseline)

12 of the 31 generated rules on disk are exact-only and were being widened. Not an outage — the
safety spine catches it, because the legit probe is blocked too, so validation fails and rolls
back. The cost is effectiveness: an exact-path band-aid could not pass validation on the first
attempt, burning refine attempts or being reported as a failure when the policy was correct and
normalization broke it.

The nested merge exists to supply required STRUCTURAL sub-keys, not a matching value, so the fix
drops only the inherited catch-all when the rule named a path itself. A rule naming NO path still
gets `prefix_values: ["/"]` — that is what makes `generate`'s trailing allow-all work against
XC's default-deny.

Tests pin both directions, written before the fix: exact/regex/suffix-only lose the catch-all;
an explicit prefix, prefix+exact together, a no-path rule, the trailing allow-all, the structural
sub-keys and every other nested matcher are unchanged. Diffed old vs new normalization over all
15 service-policy artifacts on disk: 12 changed, 3 identical, and every changed rule differed
ONLY in `path` with all other fields byte-identical. 200 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@henleda
henleda merged commit bede8e7 into main Jul 27, 2026
4 checks passed
@henleda
henleda deleted the fix-normalize-path-widening branch July 27, 2026 14:36
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