refactor: remove M3-2 legacy operator_key fallback (#87 item 3)#172
Draft
Augustas11 wants to merge 9 commits into
Draft
refactor: remove M3-2 legacy operator_key fallback (#87 item 3)#172Augustas11 wants to merge 9 commits into
Augustas11 wants to merge 9 commits into
Conversation
…bearer matcher Step 1 of #87 item 3 (legacy fallback removal). The 30-day clean-cutover gate at 2026-07-12 is the merge trigger; this PR is prepared in advance to capture the work atomically. - Removes BearerKindOperatorKey enum value (no caller can produce it post-cutover); String() simplifies to a single-arm conditional. - Simplifies GatewayInternalBearerMatches signature from (headers, operatorKey, serviceToken) -> (headers, serviceToken). Empty serviceToken still means DENY (M1-5 / SECU-5 preserved). - Drops the internalAuthKey field from buyer.Server (no longer referenced after the matcher signature change). Compilation will break in callers (WithInternalAuthKey option, internalBearerAuthorizedFull, tokens_test, server_bridge_test, main_test); follow-up commits land those + the gateway-side UpstreamCoordinatorBearer simplification + Validate + docs in the same PR.
…m 3) Step 2 of #87 item 3, building on the auth/buyer base in f17e998. The 30-day clean-cutover gate fires 2026-07-12 (Pearl audit-log proof of zero gateway-origin operator_key admits on /internal/* throughout the window); this PR is prepared in advance to land atomically on the trigger date. Source changes (drop dual-credential bridge): - phase5-gateway/internal/config/config.go: UpstreamCoordinatorBearer() now returns ServiceToken directly (no fallback to OperatorKey). Validate() now requires Coordinator.ServiceToken non-empty. ServiceToken field doc rewritten ("optional" -> "REQUIRED"). - phase4-coordinator/internal/buyer/server.go: Removed WithInternalAuthKey option + internalAuthKey field. internalBearerAuthorizedFull now calls GatewayInternalBearerMatches(headers, s.gatewayServiceToken). WithGatewayServiceToken doc rewritten ("secondary" -> "REQUIRED"). Removed TODO(m3-2-cleanup) markers. - phase4-coordinator/internal/ws/server.go: Removed TODO(m3-2-cleanup) comment on authorizedOperator (the function itself was already single-credential by design). - phase4-coordinator/cmd/coordinator/main.go: Dropped buyer.WithInternalAuthKey() option call. Test updates: - phase4-coordinator/internal/auth/tokens_test.go: TestGatewayInternalBearerMatchesScoping rewritten to assert service_token accepted, operator-key-shaped bearer REJECTED. TestGatewayInternalBearerMatchesEmptyServiceToken pins M1-5 DENY invariant under single-credential mode. TestGatewayInternalBearerMatchesEvaluatesBoth deleted (the timing-oracle concern is moot with a single credential branch). - phase4-coordinator/internal/buyer/server_bridge_test.go: Rewritten end-to-end: operator-key-shaped bearer on /internal/ routing + /internal/sticky now MUST 401. Audit-log assertion confirms key=operator_key NEVER appears on /internal/* post-cutover. - phase4-coordinator/internal/buyer/server_test.go (15 sites): Bulk-renamed WithInternalAuthKey -> WithGatewayServiceToken; the literal "operator-key" is preserved as the service-token value so Bearer assertions still match. - phase4-coordinator/cmd/coordinator/main_test.go: reloadTestServersWithLogger now seeds GatewayServiceToken = OperatorKey so the fetchReloadTier2Metadata helper continues to admit with Bearer "operator-key" without threading a new constant. - phase5-gateway/internal/config/config_test.go: validTestConfig helper now seeds Coordinator.ServiceToken so every test using it survives the new Validate gate. - phase5-gateway/internal/config/config_bridge_test.go: TestUpstreamCoordinatorBearerFallsBackToOperatorKey rewritten as TestUpstreamCoordinatorBearerNoFallbackToOperatorKey — locks the empty-ServiceToken case returning "" (not OperatorKey). - phase5-gateway/internal/router/server_test.go: newTestHarnessConfig seeds ServiceToken; 4 upstream-bearer assertions on /internal/routing + /internal/sticky updated from "Bearer operator-key" to "Bearer service-token". /poolz and /admin/* assertions stay on operator-key (those routes are operator-only by design and weren't part of the M3-2 bridge). - test/integration/scenarios_test.go: TestInternalBearerOperatorKeyFallbackAccepted rewritten as TestInternalBearerOperatorKeyRejectedPostCutover. TestGatewayOperatorKeyFallbackEndToEnd removed with a stub comment pointing at the replacement + reasoning. Audit closeout: - audits/2026-06-10/M3-2_LEGACY_FALLBACK_REMOVAL.md: Status header added marking the tracker as REMOVED in this PR. Test runs (in worktree at ../macprovider-m3-2-fallback-removal): - (cd phase4-coordinator && go test ./...) green - (cd phase5-gateway && go test ./...) green - (cd test/integration && go test ./...) green Hold for merge on 2026-07-12 (issue #87 30-day-clean-cutover gate); a final smoke check on Pearl (journalctl proof of continued zero legacy admits) is the merge precondition.
…eploy gate 3-of-3 codex audit lanes (code/security/architect) converged on two MEDIUM findings against PR #172 HEAD 409a25e: 1. Coordinator-side Validate gap: gateway_service_token is the SOLE accepted credential on /internal/* post-removal, but config.Validate() still only required operator_key. A deploy with missing auth.gateway_service_token would boot the coordinator and 401 every /internal/routing + /internal/sticky call. 2. Operator/service token distinctness invariant: nothing prevented auth.operator_key == auth.gateway_service_token (or the gateway-side equivalent). Equal values collapse the operator-vs-service credential split this PR was meant to finish — the operator credential would still authenticate /internal/* by value. Fixes: * phase4-coordinator/internal/config/config.go - Validate() now requires Auth.GatewayServiceToken non-empty. - Validate() now rejects Auth.GatewayServiceToken == Auth.OperatorKey. - GatewayServiceToken doc rewritten optional/bridge-era -> REQUIRED. * phase5-gateway/internal/config/config.go - Validate() now rejects Coordinator.ServiceToken == Coordinator.OperatorKey. - gofmt-clean (blank doc-line on CoordinatorHeaderTimeout). * phase4-coordinator/dist/check-deploy-config.sh - New required hex check: coordinator auth.gateway_service_token. - Flipped gateway coordinator.service_token from OPTIONAL to REQUIRED. - New C2c distinctness gate: coordinator self, gateway self, and cross-file (gateway operator_key vs coordinator service_token). - env:NAME tokens that do not resolve at gate time skip distinctness (deferred to runtime Validate) rather than false-fail. * phase4-coordinator/dist/test/check_deploy_config_test.sh - Helpers seed distinct HEX64B service tokens by default. - T14 flipped: gateway service_token absent -> FAIL (was: pass). - New T26..T30 cover C2c paths (missing token, intra-file collapse on each side, cross-file collapse, all-deferred-skipped). - 63/63 assertions green. * phase4-coordinator/internal/config/{config_env,config_explorer,config}_test.go, phase4-coordinator/internal/explorer/handlers_test.go, phase4-coordinator/cmd/coordinator/main_test.go - Test fixtures now seed a distinct GatewayServiceToken alongside OperatorKey so the new Validate gates pass. - reloadTestServersWithLogger: drop OperatorKey collapse (would now trip distinctness); fetchReloadTier2Metadata sends Bearer "gateway-service-token" instead. Test runs (in worktree): - phase4-coordinator: go test ./... -count=1 green - phase5-gateway: go test ./... -count=1 green - test/integration: go test ./... -count=1 green - phase4-coordinator/dist/test/check_deploy_config_test.sh 63/63 PASS Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes; r2 will re-fire to verify. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3-lane codex audit r2 against PR #172 ee066a1 (the r1 fix) surfaced: * 3-of-3 convergent MEDIUM — cross-file PAIRING check missing. The gateway sends Coordinator.ServiceToken on /internal/*; the coordinator accepts ONLY its auth.gateway_service_token. r1 enforced distinctness (op != svc) on each side but never verified the two service-token fields actually MATCH across the file boundary. A deploy with two individually-valid, intra-file-distinct 64-hex tokens that happen to disagree across coordinator.yaml / gateway.yaml passes both module Validate()s and the deploy gate, then 401s every /internal/routing + /internal/sticky call. * Code lane (1 of 3) MEDIUM — same-env-NAME bypass of distinctness. The r1 _check_distinct skipped when _resolved_value returned None for an unresolved env:NAME. But env:SHARED on BOTH sides resolves to the same value at runtime — a static catch is possible from the raw value before resolution. * Code lane (1 of 3) MEDIUM — whitespace-equivalent bypass of runtime Validate. auth.BearerTokenMatchesHeader trims both sides before comparing, so operator_key:"X" and gateway_service_token:"X " pass the r1 strict-== distinctness check but collapse to the same value on the wire. Fixes: * phase4-coordinator/internal/config/config.go - Distinctness compare now TrimSpace-normalized (mirrors the runtime bearer match). * phase5-gateway/internal/config/config.go - Same TrimSpace normalization on the gateway-side distinctness. * phase4-coordinator/dist/check-deploy-config.sh - New _env_name() helper extracts the NAME from an env:NAME ref. - _check_distinct() now hard-fails when both sides reference the same env:NAME (static catch — no resolution needed). - _resolved_value() now TrimSpace-normalizes the resolved value so the gate judges the same way the runtime will. - New _check_pair_equal() helper asserts two fields hold the SAME secret; same-env-NAME both sides counts as a match. - Wired pairing: gateway coordinator.service_token MUST equal coordinator auth.gateway_service_token (when resolvable). * phase4-coordinator/dist/test/check_deploy_config_test.sh - T31: same env:NAME on both sides -> FAIL (static). - T32: pairing inline mismatch -> FAIL. - T33: pairing env-resolved mismatch -> FAIL. - T34: pairing match via different env names same value -> pass. - 71/71 PASS. * phase4-coordinator/internal/config/config_test.go - TestValidateRejectsWhitespaceEquivalentServiceToken: 4 cases (trailing space, leading space, trailing newline, both padded) all must reject. Pins the runtime distinctness fix. * phase5-gateway/internal/config/config_test.go - Symmetric test on gateway-side Validate (3 cases). Test runs (in worktree): - phase4-coordinator: go test ./... -count=1 green - phase5-gateway: go test ./... -count=1 green - test/integration: go test ./... -count=1 green - deploy gate harness: 71/71 PASS Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes; r3 will re-fire to verify all three MEDIUMs are closed and no new ones introduced. MINORs about stale docs/comments still deferred per the "MINORs non-blocking" convention. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3-of-3 codex lanes converged on the same MEDIUM in r2:
The r2 deploy gate treated same `env:NAME` on both sides as proof of
same value at runtime. That is true within ONE yaml file (same systemd
env file is sourced) but FALSE across files: the coordinator unit
sources /etc/macprovider/coordinator.env while the gateway unit
sources /etc/macprovider/gateway.env, and the two files can define
the same variable differently.
Two concrete hazards in r2:
- C2c pairing of gateway coordinator.service_token vs coord
auth.gateway_service_token wrongly declared "match" when both
were `env:SHARED` — the gateway and coordinator can boot with
different actual tokens and 401 every /internal/* call.
- Cross-file distinctness (gw operator_key vs coord
gateway_service_token) wrongly hard-failed safe deploys where
both sides used `env:SHARED` deliberately with distinct values
across the two env files.
Fix — add `same_file=` to the two C2c helpers:
* phase4-coordinator/dist/check-deploy-config.sh
- _check_distinct(.., same_file): same env:NAME is static fail only
when both fields share a yaml file (and therefore a runtime env
file). Cross-file: skip with explicit "separate env files" note
and let runtime Validate() backstop the in-file case on each side.
- _check_pair_equal(.., same_file): same env:NAME is "proven match"
only when same_file=True. Cross-file unresolved pair with same
env:NAME emits a LOUD WARN (not silent skip): pairing is the
load-bearing /internal/* invariant; the gate cannot read both
env files, but the operator must verify manually.
- Wired call sites: coord self (True), gw self (True), gw op vs
coord svc (False), pairing (False).
* phase4-coordinator/dist/test/check_deploy_config_test.sh
- T31 redesigned: same env:NAME within coordinator.yaml -> FAIL
(same-file static catch). Was previously cross-file.
- T35 new: pairing cross-file same env:NAME unresolved -> pass
with explicit UNVERIFIED warn ("SEPARATE env files" message).
- T36 new: cross-file distinctness same env:NAME unresolved ->
skipped (not a false-fail).
- T30 substring updated for the new skip message wording.
- 78/78 PASS.
Test runs (in worktree):
- phase4-coordinator: go test ./... -count=1 green (no Go change)
- phase5-gateway: go test ./... -count=1 green
- test/integration: go test ./... -count=1 green
- deploy gate harness: 78/78 PASS
Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes;
r4 will verify the env-file scoping closed the convergent r3 MEDIUM
and check whether anything else surfaced. MINORs (stale fallback-era
docs/comments) still deferred per the "MINORs non-blocking" rule.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3-of-3 codex lanes converged on r3 MEDIUM:
r3 only WARNed cross-file pairing when both sides shared an env:NAME.
All OTHER unresolved cross-file cases (inline+env-unresolved,
different-env-unresolved, cross-file distinctness with any unresolved
side) printed `ok: skipped` and exited 0. Misleading: those have NO
runtime backstop — neither coordinator nor gateway Validate() can see
the other's token. A mismatched env file boots green and 401s every
/internal/* call.
The r3 message also said "each side fail-closes in its own Validate"
for cross-file skips, which was literally false: in-file Validate
cannot enforce a cross-file invariant.
Fix:
* phase4-coordinator/dist/check-deploy-config.sh
- _check_distinct(same_file=False): unresolved -> WARN UNVERIFIED,
explicit "NO runtime backstop" wording. Same-file unresolved still
ok-skips with accurate "module Validate enforces distinctness".
- _check_pair_equal(same_file=False): unresolved (any reason) ->
WARN UNVERIFIED, with detail naming env-NAME or raw fields.
Suggests source-both, inline-one, or SKIP_PAIRING_CHECK=1 (the
escape valve is mentioned but not implemented — operators can add
it later if they need to silence the WARN).
- Same-file pairing unresolved still ok-skips (resolves to one
value at runtime via shared Validate path).
* phase4-coordinator/dist/test/check_deploy_config_test.sh
- T36 rewritten: cross-file distinctness unresolved -> WARN
(was: ok skip with "separate env files").
- T37 new: pairing inline+env-unresolved -> WARN.
- T38 new: pairing both-env-unresolved diff names -> WARN.
- 83/83 PASS.
Test runs (in worktree):
- phase4-coordinator: go test ./... -count=1 green
- phase5-gateway: go test ./... -count=1 green
- test/integration: go test ./... -count=1 green
- deploy gate harness: 83/83 PASS
Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes;
r5 will re-fire to verify the WARN scope is complete and check whether
anything new surfaced. MINORs (stale fallback-era docs/comments) still
deferred per the "MINORs non-blocking" rule.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ount
3-of-3 codex lanes converged on a security finding (security lane
elevated it to HIGH; code/architect rated MEDIUM):
HIGH/MEDIUM — WARN path printed inline secret values via
`raw_a={raw_a!r} raw_b={raw_b!r}`. Targeted probe confirmed a live
64-hex gateway service_token was leaked into deploy output when the
coordinator side used env:NAME unresolved.
Plus 3-of-3 recurring MINORs (kept rolling into each audit):
- WARN guidance named an unimplemented SKIP_PAIRING_CHECK=1 var.
- Final pass line ("config-drift check passed") hid any WARN count.
Fixes:
* phase4-coordinator/dist/check-deploy-config.sh
- New _safe_describe(raw): classifies a secret field as one of
'absent' | 'inline-redacted' | 'env:NAME (resolved|unresolved)' |
'env malformed'. NEVER prints the raw value.
- _check_pair_equal cross-file unresolved WARN now uses _safe_describe
for both sides; removed all raw_a/raw_b interpolation.
- WARN guidance replaces SKIP_PAIRING_CHECK=1 with a concrete
manual smoke check (curl /internal/routing with the gateway
service token; 401 = mismatch).
- Added `warns` counter (parallel to existing `fail`). End-of-gate
summary line now prints "config-drift summary: N WARN(s) — review
above; ..." plus a manual-verification prompt when N > 0, or a
bare "0 WARN(s)" when clean.
* phase4-coordinator/dist/test/check_deploy_config_test.sh
- T37 strengthened with three new asserts:
* assert_absent "$HEX64B" — regression guard for the secret leak.
* "inline-redacted" present.
* "env:COORD_SVC (unresolved)" present.
- T39 new: WARN-bearing run surfaces "config-drift summary: N
WARN(s)" + "manual verification" prompt.
- 90/90 PASS (Pylib trip-up on T39/T40 zero-WARN case fixed by
asserting only the line shape, since the realistic baseline
always emits some defaulting-field WARNs).
Test runs (in worktree):
- phase4-coordinator: go test ./... -count=1 green
- phase5-gateway: go test ./... -count=1 green
- test/integration: go test ./... -count=1 green
- deploy gate harness: 90/90 PASS
Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes;
r6 will verify the redaction is complete and check whether anything
new surfaced. The doc-drift MINOR (stale fallback wording in YAML
examples and router comments) has now recurred 5 audit rounds — still
non-blocking but worth a dedicated cleanup pass after convergence.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2-of-3 codex lanes (security + code, architect rated MINOR) elevated
to HIGH: r5 redacted _check_pair_equal but a second secret-leak path
remained inside check_hex_secret(). Two distinct branches were
echoing the raw secret-bearing field:
(a) Malformed env path (`env:NAME` where NAME doesn't match the
identifier regex). Probe trigger: `env:0123...` — HEX64 starts
with '0', a digit, so ENV_REF rejects NAME. Hard-fail line
printed `{raw!r}` -> full bearer value reached the deploy log.
(b) Valid-env-name DEFERRED path. Probe trigger: `env:fedcba...` —
HEX64B starts with 'f', a letter, so ENV_REF accepts NAME as a
"valid env var name". The deferred-OK line then echoed
`deferred to runtime via env:fedcba...` -> full bearer value in
the log even on the success path.
Fixes:
* phase4-coordinator/dist/check-deploy-config.sh
- Malformed branch (path a): replaced `{raw!r}` with the fixed
string "malformed env indirection (value redacted); expected
env:NAME where NAME matches [A-Za-z_][A-Za-z0-9_]*".
- Deferred branch (path b): NEW secret-shape sniff on the env
NAME. If len(NAME) >= 32 AND NAME is pure hex, hard-fail with a
redacted "suspected secret-value typo" message. Real env vars
use SCREAMING_SNAKE_CASE (e.g. COORDINATOR_OPERATOR_KEY) and
never collide with this heuristic; only an operator typo that
put the secret value where a variable name belongs trips it.
* phase4-coordinator/dist/test/check_deploy_config_test.sh
- T7a/T7b strengthened: "value redacted" present, raw value
absent.
- T7c new (two sub-cases): probes BOTH leak paths with HEX64 and
HEX64B sentinels and asserts neither value appears in output.
- 98/98 PASS.
Test runs (in worktree):
- phase4-coordinator: go test ./... -count=1 green
- phase5-gateway: go test ./... -count=1 green
- test/integration: go test ./... -count=1 green
- deploy gate harness: 98/98 PASS
Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes;
r7 will verify both redaction paths and check for any other secret-
bearing fields printed via raw interpolation. Architect lane already
declared CONVERGED at r6; this commit closes the security+code HIGH.
Doc-drift MINOR (stale fallback wording) now in its 6th audit round —
still non-blocking; worth a dedicated cleanup pass after convergence.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2-of-3 codex lanes (security + code, both HIGH) found r6 incomplete:
hard() does NOT exit, so after `check_hex_secret` redacted the first
failure of a typoed `env:<hex-secret>`, the script continued and the
SAME env name leaked through downstream C2c paths:
- _safe_describe printed `env:{name}` directly.
- _check_distinct same-env-NAME hard-fail printed `env:{na}`.
- _check_pair_equal same-env-NAME pairing ok/warn printed `env:{na}`.
Probe trigger: `service_token: env:<64-hex-letter-leading>`. The check
hex_secret hard-fail showed "suspected secret-value typo" with safe
wording, then the next-pass cross-file pairing WARN echoed the full
`env:fedcba9876...` back into the deploy log.
Fix — centralize redaction in a single helper, use everywhere:
* phase4-coordinator/dist/check-deploy-config.sh
- New top-level `_safe_env_name(name)` defined BEFORE check_hex_secret
so all secret-emitting paths share it without ordering hazards.
Returns "<redacted: hex-shaped env name, possible secret typo>"
when len(name) >= 32 and pure hex; verbatim otherwise.
- check_hex_secret deferred-OK + resolved-from lines now use
`name_display = _safe_env_name(name)` instead of raw `name`.
- _safe_describe routes its env:NAME render through _safe_env_name.
- _check_distinct same-file same-env hard-fail uses _safe_env_name.
- _check_pair_equal same-file ok and cross-file pairing-detail
messages both use _safe_env_name.
* phase4-coordinator/dist/test/check_deploy_config_test.sh
- T7c expanded from 2 sub-cases to 4 (a/b/c/d): each of the four
check_hex_secret callers (coord operator_key, coord
gateway_service_token, gw operator_key, gw service_token)
probed with `env:$HEX64B` typo. assert_absent on the full
output (not just the first failure line) — the regression
property is FULL-OUTPUT-absent, not per-line redaction.
- 102/102 PASS.
Test runs (in worktree):
- phase4-coordinator: go test ./... -count=1 green
- phase5-gateway: go test ./... -count=1 green
- test/integration: go test ./... -count=1 green
- deploy gate harness: 102/102 PASS
Convergence target remains 0 CRITICAL/HIGH/MEDIUM across all 3 lanes.
Architect r7 already declared CONVERGED before this commit (didn't
catch the deferred leak path). r8 will verify the centralized helper
covers every secret-bearing emission and check whether anything new
surfaced from the helper extraction.
Doc-drift MINOR (stale fallback wording in router comments + yaml
examples) is now in its 7th audit round, still non-blocking. Will
land as a follow-up after convergence.
Co-Authored-By: Claude Opus 4.7 <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.
Summary
Closes the code half of issue #87 item 3 — removes the M3-2 dual-credential bridge so
gateway_service_tokenis the SOLE accepted credential on coordinator/internal/routingand/internal/sticky.DRAFT — held for merge on 2026-07-12 per the 30-day-clean-cutover gate the operator set on #87 item 3. The branch is prepared in advance and the test suite is green; the merge precondition is a final Pearl audit-log smoke check on or after 2026-07-12 (
journalctl -u macprovider-coordinator --since "24h ago" | grep -E 'internal_bearer_accepted.*"key":"operator_key".*"path":"/internal/' | wc -lmust read 0).Pre-conditions met (verified 2026-06-26)
operator_keyadmits on/internal/*in 24h; 20service_tokenadmits in same window. Re-verified at 04:02 UTC and again at 09:30 UTC.OPERATOR_KEYrotation): EXECUTED 2026-06-26 04:02 UTC on Pearl. New 32-byte hex generated server-side, written tocoordinator.env+gateway.env, both services restarted clean./poolzverified with new key.Trigger
The 30-day clock starts from the M3-2 cutover (2026-06-12 PR #73) → fires 2026-07-12. Do not squash this PR before then.
What changes
Code (drop the bridge):
phase5-gateway/internal/config/config.goUpstreamCoordinatorBearer()returnsServiceTokendirectly; no fallback toOperatorKey.Validate()requiresCoordinator.ServiceTokennon-empty (post-cutover the gateway can't route any /internal/* traffic without it).ServiceTokenfield doc: "optional" → "REQUIRED".phase4-coordinator/internal/auth/tokens.goGatewayInternalBearerMatchessignature(headers, operatorKey, serviceToken)→(headers, serviceToken).BearerKindOperatorKeyenum value removed;String()collapses to a one-line conditional.phase4-coordinator/internal/buyer/server.goWithInternalAuthKeyOption +internalAuthKeyfield.internalBearerAuthorizedFullupdated to single-credential call.WithGatewayServiceTokendoc rewritten ("secondary" → "REQUIRED").phase4-coordinator/internal/ws/server.goTODO(m3-2-cleanup)block onauthorizedOperator(function itself unchanged — operator-only by design).phase4-coordinator/cmd/coordinator/main.gobuyer.WithInternalAuthKey()call.Tests (rewrite to assert post-cutover reject):
auth/tokens_test.go:TestGatewayInternalBearerMatchesScopingrewritten — operator-key-shaped bearer now REJECTED.TestGatewayInternalBearerMatchesEmptyServiceTokennew (M1-5 DENY pin under single-credential mode).TestGatewayInternalBearerMatchesEvaluatesBothdeleted (timing-oracle moot post-removal).buyer/server_bridge_test.go: Rewritten end-to-end. Operator-key-shaped bearer on/internal/routing+/internal/stickyMUST 401. Audit-log assertion confirmskey=operator_keyNEVER appears on/internal/*post-cutover.buyer/server_test.go(15 sites): Bulk-renamedWithInternalAuthKey→WithGatewayServiceToken. The literal"operator-key"is preserved as the service-token value so Bearer assertions still match without literal churn.cmd/coordinator/main_test.go:reloadTestServersWithLoggerseedsGatewayServiceToken = OperatorKeyso the helper-internalfetchReloadTier2Metadatacontinues admitting withBearer "operator-key".phase5-gateway/internal/config/config_test.go:validTestConfighelper now seedsCoordinator.ServiceTokenfor the newValidategate.phase5-gateway/internal/config/config_bridge_test.go:TestUpstreamCoordinatorBearerFallsBackToOperatorKeyrewritten asTestUpstreamCoordinatorBearerNoFallbackToOperatorKey— locks the empty-ServiceToken case returning""(not OperatorKey).phase5-gateway/internal/router/server_test.go:newTestHarnessConfigseedsServiceToken. 4 upstream-bearer assertions on/internal/routingand/internal/stickyupdatedBearer operator-key→Bearer service-token./poolz+/admin/*assertions stayoperator-key(those routes are operator-only and weren't part of the M3-2 bridge).test/integration/scenarios_test.go:TestInternalBearerOperatorKeyFallbackAcceptedrewritten asTestInternalBearerOperatorKeyRejectedPostCutover.TestGatewayOperatorKeyFallbackEndToEndremoved with a stub comment pointing at the replacement + reasoning.Audit closeout:
audits/2026-06-10/M3-2_LEGACY_FALLBACK_REMOVAL.mdgets a status header at the top marking the tracker as REMOVED in this PR.Test plan
(cd phase4-coordinator && go test ./... -count=1)green(cd phase5-gateway && go test ./... -count=1)green(cd test/integration && go test ./... -count=1)greenservice_tokenconfiguredPre-PR audit-loop discipline
This PR is a removal/deletion against verified-zero-usage production code, not a new abstraction or behavior. Per the audit-loop trigger logic, a single codex pass on the diff is sufficient (not a full IMPL loop). I'll fire that before un-drafting on 2026-07-12.
🤖 Generated with Claude Code