--- id: FLEX-WP-0023 type: workplan title: "Operator caller access path and caller identity in the decision record" domain: infotech repo: flex-auth status: active owner: claude topic_slug: netkingdom planning_priority: P1 planning_order: 230 depends_on_workplans: - FLEX-WP-0021 related_workplans: - FLEX-WP-0016 - SECRETS-WP-0009 created: "2026-09-06" updated: "2026-09-06" state_hub_workstream_id: "ad011f92-786c-51ad-b3f6-c06ad77e7af7" --- # FLEX-WP-0023 — Operator caller access path and caller identity in the decision record Opened by `glas-harness` (`GLAS-WP-0015`, 2026-09-06), which asked flex-auth to keep the access-path gate live work despite `FLEX-WP-0021-T05` closing, and to return "a supported owner access path and authenticated caller binding/lifetime with positive and negative tests". They ruled out Service DNS and a permanent operator token by name. Both refusals are correct. Design: [`../docs/operator-caller-access-path.md`](../docs/operator-caller-access-path.md). Contract gap: `FLEX-DEC-2026-009`. ## The finding that reframes the task For an operator caller the pin's default-deny `NetworkPolicy` is not a partial control, it is **silent**: `kubectl port-forward` is proxied to the pod's own loopback and no policy selector is consulted. So with `callerAuth.mode: warn`, the operator path is unauthenticated *and* unfiltered, and the only reason it is not reachable today is that nobody has forwarded the port. `warn` was a safe migration state for `ops-warden` because a workload pin still admitted exactly one pod. Here the warn window protects nothing while it runs. **`enforce` is the deliverable, not the follow-up.** ## 1. Create the ServiceAccount the deployed binding already names ```task id: FLEX-WP-0023-T01 status: todo priority: high state_hub_task_id: "10e5a40c-f142-55b9-ab15-fc77c0064ce0" ``` Owner: `flex-auth`; production write, operator approval required. The pin runs with `--caller-binding secrets-engine=system:serviceaccount:secrets-engine:secrets-engine` and namespace `secrets-engine` holds only `default`. There is nothing to mint a token for, and `enforce` today would deny every request rather than authenticate one. - Create ServiceAccount `secrets-engine` in namespace `secrets-engine`. - No `Role`, no `RoleBinding`, no `automountServiceAccountToken`. An SA with no binding grants nothing in the cluster; its only function is to be the name in the binding above. - Add it to the overlay rather than applying it loose, following `deploy/caller-auth-rbac.yaml`. Gate: `kubectl -n secrets-engine create token secrets-engine --audience=flex-auth --duration=10m` returns a token whose `sub` equals the bound principal exactly. ## 2. Run the positive and negative tests and return the receipts ```task id: FLEX-WP-0023-T02 status: wait priority: high state_hub_task_id: "79a8d82d-777c-5462-b49d-098f4b7a3b9c" ``` Owner: `flex-auth` to run; `glas-harness` to receive. Procedure and expectations are in the design doc. One positive (correct SA, correct audience, inside lifetime, expecting allow at `policy_version: v2`) and four negatives, each isolating one property: | # | Credential | Expected under `enforce` | | --- | --- | --- | | N1 | no `Authorization` header | 401 | | N2 | token for `secrets-engine:default` | 403, principal cannot represent system | | N3 | token minted without `--audience=flex-auth` | 401 | | N4 | token past `exp` | 401 | N2 and N4 are the load-bearing ones. N2 proves the binding is a binding rather than mere possession of a valid cluster token — thousands of ServiceAccounts can produce a well-formed one and only one may represent `secrets-engine`. N4 proves the lifetime is enforced by the issuer rather than asserted by the caller. **Blocked on credential issuance, not on design.** `kubectl create token` is credential minting and was refused in the 2026-09-06 session that wrote this plan, correctly. The commands are exact and the expectations are read out of `internal/callerauth/auth.go`; running them is an operator action. Under `warn` all four negatives are *allowed*, so running them before `T03` records the finding rather than a passing test. Gate: five receipts returned to `glas-harness`, each naming which property it isolates. Nothing is reported as verified that was not run. ## 3. Flip `callerAuth.mode` to enforce ```task id: FLEX-WP-0023-T03 status: wait priority: high state_hub_task_id: "8117c9d8-6efa-5ccf-8ed4-4da2519c3de3" ``` Owner: `flex-auth`; deployment approval required. - Flip only the `flex-auth-secrets-engine` pin. Do not roll the other three; they are pinned to their own digests so one change does not move another consumer (`FLEX-WP-0016`). - Confirm the warn logs are clean for the adopted identity first, as `FLEX-WP-0016` did for `ops-warden` — but keep the window short, per the finding above. - Re-run `T02`'s four negatives after the flip. Before it they document the gap; after it they are the test. Gate: N1–N4 refused, positive still allowed, other three pins unchanged. `glas-harness`'s standing instruction — do not turn warn into enforce before caller adoption is proved — is satisfied by `T02`, not bypassed by this task. ## 4. Record the authenticated caller in the decision record ```task id: FLEX-WP-0023-T04 status: todo priority: high state_hub_task_id: "c0e4f31a-cc42-5bd2-b938-d140ecd52e1a" ``` Owner: `flex-auth`. `FLEX-DEC-2026-009`: the verification in `T02`/`T03` is real and leaves no trace. `flex-auth.decision-record.v1` has no caller field, so a record proves the subject was allowed and cannot prove the caller who obtained it was authenticated. - Add `provenance.caller` — `mode` (required), `principal`, `audience`, `not_after`. Not `binding`: the caller is deliberately not decision material, and putting it there would change `request_digest` and break every consumer's replay join. - `mode` is load-bearing. A `principal` recorded under `warn` was observed, not enforced, and a reader who cannot tell those apart reads an unverified string as verification. Under `disabled`, emit `{"mode": "disabled"}` — absence stated, following the `pdp_digest` precedent. - Capture the reviewed token's `exp` for `not_after`. `internal/callerauth.Identity` carries only `Username` and `Audiences`, so the expiry is validated by `TokenReview` and then discarded. This is the one real code change. - Update `schemas/decision_envelope.schema.json` and `docs/decision-record-contract.md`; state in both that `request_digest` is unaffected, so nobody re-pins in response. Gate: a decision obtained under `enforce` names its caller and lifetime; the same request's `request_digest` is byte-identical to the pre-change value. ## 5. Report the gap to gate-house as a v0.8 finding ```task id: FLEX-WP-0023-T05 status: wait priority: medium state_hub_task_id: "d685abfc-cf86-50c8-ad5e-2c04e1531ddd" ``` Owner: `flex-auth`. §17 makes the decision-record schema flex-auth's, so this gap is ours to have missed — and the standard does not ask for what §9.7.2's own argument implies. §9.7.2 promoted registry-snapshot provenance to a conformance prerequisite because a decision turning on registry content must be replayable from its own record. **A decision gated by caller authentication is not auditable from its own record by the same argument.** Carry it into the outstanding v0.8 assent review rather than as a separate message.