You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MGMT_API_KEY is shared with every caller container and X-Spiffe-Admin-Key survives the interceptor - scope both so /mgmt reachability can't become a live policy rewrite #44
The failing catalog in #43 pins two HTTP framing paths that can forward a second unauthorized request, with the product fixes to follow as separate regression-driven changes. That catalog is the right way to land those fixes, and nothing here disputes it. This item is about the management API those escapes reach, and about two deployment-shape facts that will still be true after the framing fixes merge — so it reads as hardening that should land alongside them, not as a new framing report.
The two deployment facts
1. Every caller container receives the same management credential. The env-injection block of deploy.sh appends MGMT_API_KEY to each of:
if agent_name in ('budget-backend', 'budget-approval', 'budget-report', 'employee-menus', 'admin-control-plane') and mgmt_api_key:
env_list.append({'name': 'MGMT_API_KEY', 'value': mgmt_api_key})
So the read-only budget-report workload (and every other business caller) ships with the exact shared secret that authorizes PUT /mgmt/policy — and the mTLS allowlist, risk, and tag endpoints behind the same check in budget-backend/app.py.
2. The interceptor preserves the admin credential on governed requests, unconditionally.src/spiffe-proxy/internal/inspect/http.go (InjectHeaders) strips the spoofable X-SPIFFE-* identity headers but deliberately allows X-Spiffe-Admin-Key through — the allow-list #12 added so admin-control-plane's /mgmt/* calls and the portal health probe keep working. Preservation is not conditioned on the authenticated caller: it applies to every request the interceptor forwards.
What the composition is worth at the management API
Observed on a local run against pinned main (ab39fb5), real interceptor and management-server bytes, shipped spiffe-rbac-policy.yaml with deploy-equivalent enrichment:
Controls first: a plain DELETE /budget/admin and a plain PUT /mgmt/policy are both refused at the interceptor (403) — the shipped RBAC denies hold when only one governed request is in play.
Through the same-frame framing path Add a connected E2E harness for identity enforcement #43's same_frame case pins: a second request carrying X-Spiffe-Admin-Key reached PUT /mgmt/policy and the live policy swapped — {"status":"updated","old_version":"5.0","new_version":"9.9-takeover"} — with the read-only caller's rules replaced by path "/*", methods ["*"], action allow.
The framing path is already yours (#43); the observation that matters here is that the credential the second request needed was already provisioned into the calling container's environment, and the interceptor passed it through. A full policy rewrite required no secret theft — only reachability of /mgmt/* from any governed caller.
Why this outlives the framing fixes
After the framing fixes land, the amplifier remains: any future caller-reachable path to /mgmt/* — a parsing gap in a different layer, a co-located container reaching the app's localhost proxy, a loopback bug in a caller app — converts directly into rewriting the enforcement engine's own rules rather than stopping at a business-rule bypass. The blast radius of every future escape is set by these two deployment facts, and both are cheap to shrink now, while the fixes are in flight.
Requested changes
Scope the credential. Only principals that need management authority should hold a management-authorizing secret: per-caller keys, or a single admin key held by admin-control-plane (plus budget-backend's proxy role) with the business callers receiving nothing /mgmt/* accepts. The deploy.sh comment ties the shared key to /flush-token on caller apps — that endpoint can take its own narrow secret instead of the governance one.
Condition the interceptor's preservation on the authenticated principal.InjectHeaders runs after the caller's SPIFFE ID is established, and only the admin-control-plane policy entry allows /mgmt/* — preserving X-Spiffe-Admin-Key only for that authenticated caller keeps every Portal admin seeding, Agent Management rename, spiffe-proxy admin-key fix, README portal tour #12 use case (ACP management calls, portal health probe) working while removing the credential from every other governed request, whatever it carries.
While in this area, the CA asymmetry.evaluateCA in the Go engine (internal/rbac/engine.go) skips risk enforcement when the policy cache returns no blocked levels, while src/shared/ca_evaluator.py fails closed by stated design. The Add a connected E2E harness for identity enforcement #43 catalog already lists "initial policy unavailability, missing risk ... can allow access" and Complete and accurately document Conditional Access enforcement #31 carries the fail-closed acceptance criterion — noting only that the two runtimes currently answer the empty-cache case differently, so the fix on the Go side is where the work is.
Happy to share the local transcript, or to send a PR sketching the conditional-preservation change in InjectHeaders.
Where this sits relative to #43
The failing catalog in #43 pins two HTTP framing paths that can forward a second unauthorized request, with the product fixes to follow as separate regression-driven changes. That catalog is the right way to land those fixes, and nothing here disputes it. This item is about the management API those escapes reach, and about two deployment-shape facts that will still be true after the framing fixes merge — so it reads as hardening that should land alongside them, not as a new framing report.
The two deployment facts
1. Every caller container receives the same management credential. The env-injection block of
deploy.shappendsMGMT_API_KEYto each of:So the read-only budget-report workload (and every other business caller) ships with the exact shared secret that authorizes
PUT /mgmt/policy— and the mTLS allowlist, risk, and tag endpoints behind the same check inbudget-backend/app.py.2. The interceptor preserves the admin credential on governed requests, unconditionally.
src/spiffe-proxy/internal/inspect/http.go(InjectHeaders) strips the spoofableX-SPIFFE-*identity headers but deliberately allowsX-Spiffe-Admin-Keythrough — the allow-list #12 added soadmin-control-plane's/mgmt/*calls and the portal health probe keep working. Preservation is not conditioned on the authenticated caller: it applies to every request the interceptor forwards.What the composition is worth at the management API
Observed on a local run against pinned
main(ab39fb5), real interceptor and management-server bytes, shippedspiffe-rbac-policy.yamlwith deploy-equivalent enrichment:DELETE /budget/adminand a plainPUT /mgmt/policyare both refused at the interceptor (403) — the shipped RBAC denies hold when only one governed request is in play.same_framecase pins: a second request carryingX-Spiffe-Admin-KeyreachedPUT /mgmt/policyand the live policy swapped —{"status":"updated","old_version":"5.0","new_version":"9.9-takeover"}— with the read-only caller's rules replaced bypath "/*", methods ["*"], action allow.The framing path is already yours (#43); the observation that matters here is that the credential the second request needed was already provisioned into the calling container's environment, and the interceptor passed it through. A full policy rewrite required no secret theft — only reachability of
/mgmt/*from any governed caller.Why this outlives the framing fixes
After the framing fixes land, the amplifier remains: any future caller-reachable path to
/mgmt/*— a parsing gap in a different layer, a co-located container reaching the app's localhost proxy, a loopback bug in a caller app — converts directly into rewriting the enforcement engine's own rules rather than stopping at a business-rule bypass. The blast radius of every future escape is set by these two deployment facts, and both are cheap to shrink now, while the fixes are in flight.Requested changes
admin-control-plane(plusbudget-backend's proxy role) with the business callers receiving nothing/mgmt/*accepts. Thedeploy.shcomment ties the shared key to/flush-tokenon caller apps — that endpoint can take its own narrow secret instead of the governance one.InjectHeadersruns after the caller's SPIFFE ID is established, and only theadmin-control-planepolicy entry allows/mgmt/*— preservingX-Spiffe-Admin-Keyonly for that authenticated caller keeps every Portal admin seeding, Agent Management rename, spiffe-proxy admin-key fix, README portal tour #12 use case (ACP management calls, portal health probe) working while removing the credential from every other governed request, whatever it carries.evaluateCAin the Go engine (internal/rbac/engine.go) skips risk enforcement when the policy cache returns no blocked levels, whilesrc/shared/ca_evaluator.pyfails closed by stated design. The Add a connected E2E harness for identity enforcement #43 catalog already lists "initial policy unavailability, missing risk ... can allow access" and Complete and accurately document Conditional Access enforcement #31 carries the fail-closed acceptance criterion — noting only that the two runtimes currently answer the empty-cache case differently, so the fix on the Go side is where the work is.Happy to share the local transcript, or to send a PR sketching the conditional-preservation change in
InjectHeaders.