Skip to content

Strip every X-Saturn-* header and trailer before forwarding upstream (Q-R8STRIP2) - #58

Merged
hhuuggoo merged 5 commits into
release-2026.08.01from
hugo/strip-all-saturn-headers
Oct 5, 2026
Merged

hhuuggoo merged 5 commits into
release-2026.08.01from
hugo/strip-all-saturn-headers

Conversation

@hhuuggoo

@hhuuggoo hhuuggoo commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Hugo's ruling Q-R8STRIP2 (2026-10-04): no X-Saturn-* header reaches the upstream (vLLM / Dynamo). This widens phoebe's #57 strip, which kept the trusted set and the eight edge-contract headers, into a strip of the whole namespace.

  • identity.StripUntrustedSaturnHeaders is now identity.StripSaturnHeaders. It removes every header whose name starts with X-Saturn- (case-insensitive), including trusted, edge-contract, retired and unregistered names. Names that only look similar (X-Saturnine, X-SaturnX, bare X-Saturn) are kept.
  • edgeContractHeaders, ForwardableSaturnHeaders and their oracle tests are deleted. PHOEBE_TRUSTED_HEADERS now controls only what FromRequest reads. It no longer affects what is forwarded.
  • The strip still runs in handleProxy right after identity.FromRequest, on every route. The trailer handling from Strip untrusted X-Saturn-* request headers before forwarding upstream (Q-R8STRIP b) #57 is unchanged and now strips the full namespace: the strip at body EOF and on Close, plus the Director-side strip in newUpstreamProxy.
  • Raw-header audit. No code after the strip reads an X-Saturn-* header from the raw request, with one exception, which this PR fixes. The R8 legacy-quota log diagnostic (legacyQuotaHeadersPresent(r.Header)) read raw headers after the strip. Since Strip untrusted X-Saturn-* request headers before forwarding upstream (Q-R8STRIP b) #57 it had silently stopped firing, because the strip had already removed those headers. On the current release-2026.08.01 its test, TestLegacyOnlyQuotaHeadersLogPreR8ProducerMarker, also panics: its logger has no Debug field and the strip's Debug log line dereferences it. The diagnostic now records whether the headers were present before the strip. The per-request Debug count line is removed, because the strip now removes headers on every request. Phoebe adds no X-Saturn-* header to the outbound request.

Contracts

  • After this change the upstream receives no X-Saturn-* request header or trailer on any route: dedicated header-routed, shared header-routed, gateway, and the shared wake path including its probes. This includes X-Saturn-Upstream, -Resource-Id, -Auth-Id, -Org-Id, -Serving-Mode and the rate-limit envelope. Any engine-side code that read those headers would stop seeing them. None is known.
  • Phoebe's routing, admission, wake and metering decisions use only the identity it parses before the strip.
  • This strip is the backstop to the Traefik-side strip that saturn-k8s will render from an enumerated strip list. That saturn-k8s PR does not exist yet. The PHOEBE_TRUSTED_HEADERS name and format are unchanged.

https://claude.ai/code/session_01JR59LERE7vBqUT9cga5Kvq

hhuuggoo and others added 5 commits October 4, 2026 19:21
…(Q-R8STRIP2)

Hugo's ruling Q-R8STRIP2 (2026-10-04): no X-Saturn-* header reaches the
upstream (vLLM / Dynamo). Traefik is the primary strip (saturn-k8s);
phoebe's #57 strip stays as the non-bypassable backstop and is widened
from "outside the trusted set and edge contract" to the whole X-Saturn-*
namespace, case-insensitive, in headers and trailers, on every route.

- identity: StripUntrustedSaturnHeaders -> StripSaturnHeaders (prefix
  rule); delete edgeContractHeaders / ForwardableSaturnHeaders and their
  oracle tests. The trusted-header registry now governs only what
  FromRequest reads.
- proxy: the strip runs right after FromRequest; everything after uses
  the parsed identity. The R8 legacy-quota log diagnostic read raw
  headers after the strip (it had silently stopped firing since #57, and
  its test panicked on a nil Debug logger); its presence is now recorded
  before the strip. The per-request Debug count line is dropped since
  the strip now removes headers on every request.
- tests: every route (dedicated, shared, gateway with forged routing
  headers + decoy upstream, shared via wake incl. probes) sees zero
  X-Saturn-* headers, reaches the right upstream and meters the right
  identity; over-the-wire chunked trailers carry no X-Saturn-* trailer;
  look-alikes (X-Saturnine, X-SaturnX, X-Saturn) are kept.

Claude-Session: https://claude.ai/code/session_01JR59LERE7vBqUT9cga5Kvq
Go keeps '_' in header names and WSGI/CGI-style upstreams fold '_' into
'-', so an X_Saturn_* header would read upstream as a real X-Saturn-*
header. isSaturnHeader now treats '_' as '-' in the prefix; look-alikes
(X_Saturnine, X-SaturnX) still survive. Adds a test that every Header*
constant FromRequest reads is in the stripped namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cannot race on r.Trailer

The Transport can Close the request body on one goroutine while another
reads it to EOF; both stripped r.Trailer unsynchronized, risking Go's
fatal concurrent-map-write error. A mutex now guards the strip only (never
the inner Read, so Close can still interrupt a blocked Read).
TestTrailerStripBodyConcurrentReadAndCloseDoNotRace fails under -race
without the lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gacy check without an admitter

- proxy: before the strip, log at Debug how many X-Saturn-* names are
  neither trusted nor edge-contract identity headers (the edge strip should
  have removed them). Only the count is logged. Tests:
  TestUnexpectedSaturnHeaderAtEntryLogsDebugCount (proxy),
  TestCountUnexpectedSaturnHeadersCountsOnlyNamesTheEdgeShouldHaveStripped
  (identity). The edge-contract list is restored for this count only.
- proxy: legacyQuotaHeadersPresent runs only when an admitter is set, the
  only path where its diagnostic can fire.
  TestLegacyOnlyQuotaHeadersLogPreR8ProducerMarker still proves the marker
  fires after the strip (its logger fixture gains a Debug writer).
- tests: document that isSaturnName is a deliberately independent oracle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…railer test

TestNoSaturnTrailersReachUpstreamOverWire now also drives the shared
header-routed route and the shared wake path (cold probe, re-probe, metered
forward; all three upstream requests are checked). Control trailers are
required on every upstream request that carries a body. The empty-body case
is renamed dedicated/empty-body-served-not-502 and skips the trailer loop,
since a zero-length body cannot carry trailers upstream. With the strip
disabled, every non-empty case fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hhuuggoo

hhuuggoo commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Review battery — Tier 2 — run wf_f6e1f4cc-969 — terminal status: ESCALATE after 1 round

Round 1 (sonnet): 15 raw, 15 fresh, 4 refuted, 11 confirmed, 8 persona; fixes applied. Gate green at 3cd734b: build, vet, gofmt, golangci-lint, go test -race (all packages).

Fixes pushed:

  • bdc828f — underscore spellings (X_Saturn_Owner_Id, X-Saturn_Org_Id) are stripped too: Go keeps _ in header names and some upstreams fold _ and -. Identity parsing still reads only the dash forms, so phoebe's own decisions cannot be spoofed this way.
  • 039f2c7 — trailer strips in the body wrapper are serialized so a concurrent Read/Close cannot race on r.Trailer.
  • f365120 — the R8 legacy-header check runs only when an admitter is configured; the Debug count of unexpected X-Saturn-* headers is restored.
  • 3cd734b — the over-the-wire trailer test now also covers the shared header-routed and wake routes.

Escalations and decisions (author decisions, inside Hugo's Q-R8STRIP2 ruling):

  • Underscore spellings: strip them (fail closed); identity parsing stays dash-only. No contract change: nothing upstream is meant to receive any X-Saturn-* name.
  • Trailer concurrency: keep both layers. The body wrapper strips after the server's single trailer merge at the first EOF (the mutex serializes our strips without being held across the inner Read, so Close can still interrupt a blocked Read); the outbound Director strips the request clone's own deep-copied trailer map, which covers any case the wrapper does not.

Caveat: orchestrated by the authoring session; finder, verifier and fix agents ran with fresh context.

https://claude.ai/code/session_01JR59LERE7vBqUT9cga5Kvq

@hhuuggoo
hhuuggoo merged commit c4c30ba into release-2026.08.01 Oct 5, 2026
4 checks passed
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