Skip to content

refactor: remove M3-2 legacy operator_key fallback (#87 item 3)#172

Draft
Augustas11 wants to merge 9 commits into
mainfrom
fix/m3-2-legacy-fallback-removal-v2
Draft

refactor: remove M3-2 legacy operator_key fallback (#87 item 3)#172
Augustas11 wants to merge 9 commits into
mainfrom
fix/m3-2-legacy-fallback-removal-v2

Conversation

@Augustas11

Copy link
Copy Markdown
Owner

Summary

Closes the code half of issue #87 item 3 — removes the M3-2 dual-credential bridge so gateway_service_token is the SOLE accepted credential on coordinator /internal/routing and /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 -l must read 0).

Pre-conditions met (verified 2026-06-26)

  • Item 1 of M3-2 deploy follow-ups (operator action) #87 (24h cutover-watch): PASSED — 0 legacy operator_key admits on /internal/* in 24h; 20 service_token admits in same window. Re-verified at 04:02 UTC and again at 09:30 UTC.
  • Item 2 (OPERATOR_KEY rotation): EXECUTED 2026-06-26 04:02 UTC on Pearl. New 32-byte hex generated server-side, written to coordinator.env + gateway.env, both services restarted clean. /poolz verified 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.go
    • UpstreamCoordinatorBearer() returns ServiceToken directly; no fallback to OperatorKey.
    • Validate() requires Coordinator.ServiceToken non-empty (post-cutover the gateway can't route any /internal/* traffic without it).
    • ServiceToken field doc: "optional" → "REQUIRED".
  • phase4-coordinator/internal/auth/tokens.go
    • GatewayInternalBearerMatches signature (headers, operatorKey, serviceToken)(headers, serviceToken).
    • BearerKindOperatorKey enum value removed; String() collapses to a one-line conditional.
  • phase4-coordinator/internal/buyer/server.go
    • Removed WithInternalAuthKey Option + internalAuthKey field.
    • internalBearerAuthorizedFull updated to single-credential call.
    • WithGatewayServiceToken doc rewritten ("secondary" → "REQUIRED").
  • phase4-coordinator/internal/ws/server.go
    • Removed TODO(m3-2-cleanup) block on authorizedOperator (function itself unchanged — operator-only by design).
  • phase4-coordinator/cmd/coordinator/main.go
    • Dropped buyer.WithInternalAuthKey() call.

Tests (rewrite to assert post-cutover reject):

  • auth/tokens_test.go: TestGatewayInternalBearerMatchesScoping rewritten — operator-key-shaped bearer now REJECTED. TestGatewayInternalBearerMatchesEmptyServiceToken new (M1-5 DENY pin under single-credential mode). TestGatewayInternalBearerMatchesEvaluatesBoth deleted (timing-oracle moot post-removal).
  • buyer/server_bridge_test.go: Rewritten end-to-end. Operator-key-shaped bearer on /internal/routing + /internal/sticky MUST 401. Audit-log assertion confirms key=operator_key NEVER appears on /internal/* post-cutover.
  • buyer/server_test.go (15 sites): Bulk-renamed WithInternalAuthKeyWithGatewayServiceToken. The literal "operator-key" is preserved as the service-token value so Bearer assertions still match without literal churn.
  • cmd/coordinator/main_test.go: reloadTestServersWithLogger seeds GatewayServiceToken = OperatorKey so the helper-internal fetchReloadTier2Metadata continues admitting with Bearer "operator-key".
  • phase5-gateway/internal/config/config_test.go: validTestConfig helper now seeds Coordinator.ServiceToken for 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 and /internal/sticky updated Bearer operator-keyBearer service-token. /poolz + /admin/* assertions stay operator-key (those routes are operator-only 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 gets 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) green
  • Merge precondition: re-run journalctl cutover-watch on Pearl on or after 2026-07-12; legacy admit count MUST still be 0
  • Reviewer: confirm there are no out-of-tree gateway deployments that haven't yet had service_token configured

Pre-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

…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.
Augustas11 and others added 7 commits June 26, 2026 10:21
…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>
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