Updated by fix-consistency on 2026-09-06: - workplan status: ready → active Assistant: claude-code Assistant-Model: opus Assistant-Process: 715613@bnt-lap001 Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
185 lines
7.3 KiB
Markdown
185 lines
7.3 KiB
Markdown
---
|
||
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.
|