180 lines
6.9 KiB
Markdown
180 lines
6.9 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: ready
|
|||
|
|
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"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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.
|