diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d6201ca..6db128d 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -27,6 +27,7 @@ | workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | | workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | | workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | +| workplan | WARDEN-WP-0031 | active | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | | task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | | task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | | task | ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | @@ -108,3 +109,8 @@ | task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | | task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | | task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md | +| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T04 | wait | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | +| task | WARDEN-WP-0031-T05 | wait | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | diff --git a/deploy/kubernetes/caller-identity.yaml b/deploy/kubernetes/caller-identity.yaml new file mode 100644 index 0000000..89a8b93 --- /dev/null +++ b/deploy/kubernetes/caller-identity.yaml @@ -0,0 +1,39 @@ +# ops-warden's calling identity for flex-auth (WARDEN-WP-0031 T04). +# +# flex-auth's `flex-auth-ops-warden` pin binds `resource.system: ops-warden` to +# the principal `system:serviceaccount:ops-warden:ops-warden` and TokenReviews +# the caller's bearer token with audience `flex-auth` (FLEX-WP-0016). +# +# This ServiceAccount is the subject of that binding. It holds no RBAC at all — +# it is never used to talk to the Kubernetes API, only to be *reviewed* by it. +# A workstation `warden sign` mints a short-lived bound token against it: +# +# kubectl create token ops-warden -n ops-warden \ +# --audience flex-auth --duration 10m +# +# Boundary note: cluster resources are railiance-platform's to own. This +# manifest lives here because the identity is ops-warden's and flex-auth's +# binding names it; railiance-platform should adopt it into the cluster's own +# manifests, at which point this file becomes the record of what was applied +# rather than the source of truth (ADR-0003 — cover the gap, name the owner). +apiVersion: v1 +kind: Namespace +metadata: + name: ops-warden + labels: + app.kubernetes.io/managed-by: ops-warden + netkingdom.coulomb.social/purpose: caller-identity +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ops-warden + namespace: ops-warden + labels: + app.kubernetes.io/managed-by: ops-warden + annotations: + netkingdom.coulomb.social/bound-by: >- + flex-auth-ops-warden callerAuth binding + ops-warden=system:serviceaccount:ops-warden:ops-warden (FLEX-WP-0016) + netkingdom.coulomb.social/workplan: WARDEN-WP-0031 +automountServiceAccountToken: false diff --git a/history/2026-08-19-flex-auth-caller-identity-evidence.md b/history/2026-08-19-flex-auth-caller-identity-evidence.md new file mode 100644 index 0000000..8cae128 --- /dev/null +++ b/history/2026-08-19-flex-auth-caller-identity-evidence.md @@ -0,0 +1,83 @@ +# flex-auth caller identity — live evidence (WARDEN-WP-0031 T04) + +**Date:** 2026-08-19 +**Pin:** `flex-auth-ops-warden`, railiance01 cluster, namespace `flex-auth`, +Service `flex-auth-ops-warden:8080`, digest `sha256:138aa347…`, running +`--caller-auth-mode warn --caller-kubernetes-url https://10.43.0.1 +--caller-binding ops-warden=system:serviceaccount:ops-warden:ops-warden` +(read off the live Deployment, matching FLEX-WP-0016 T02). + +Reached from the workstation by port-forward; the tunnel `k3s-api-railiance01` +(local `16444`) carries the API. Note `~/.kube/config` / `config-hosteurope` +points at `16443`, which two bridge tunnels claim +(`k3s-api-coulombcore` and `k3s-api-haskelseed`) — that collision is why a +`system:masters` cert returns `Unauthorized` there. Use +`--kubeconfig ~/.kube/config-railiance01`. + +## Baseline — before + +Unauthenticated `POST /v1/check` was **served**, and the pin logged: + +``` +caller authentication warning: caller is not authenticated +``` + +That is the whole reason `policy.enabled` could not flip: warn mode answers +anonymous callers, so nothing about the enforcing path was ever exercised. + +## What was created + +`deploy/kubernetes/caller-identity.yaml` — Namespace `ops-warden` and +ServiceAccount `ops-warden/ops-warden`, `automountServiceAccountToken: false`, +**no RBAC of any kind**. It is never used to call the Kubernetes API; it exists +only to be the subject of flex-auth's TokenReview. Applied 2026-08-19. + +Cluster resources are railiance-platform's to own — this is an ADR-0003 interim +cover, and the manifest names that owner in its header. + +## Token source + +`policy.caller_auth.mode: command` in `~/.config/warden/warden.yaml`: + +``` +kubectl --kubeconfig ~/.kube/config-railiance01 create token ops-warden \ + -n ops-warden --audience flex-auth --duration 10m +``` + +Audience `flex-auth` is required: `internal/callerauth/tokenreview.go` sends +`spec.audiences: ["flex-auth"]` and rejects an identity whose audiences do not +contain it. 900-char bound token, 10 minute TTL, minted per call, never stored. + +## Evidence + +``` +$ python3 scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090 + ✓ warden.yaml: loaded; policy.enabled=false + ✓ caller_auth.mode: command + ✓ caller token: obtained, 900 chars, sha256:e50da3ec6769 + ✓ live /v1/check: HTTP 200, effect=allow, decision=decision:f3f7c88f9585582a +READY +``` + +The decisive check is not that allow — warn would have allowed an anonymous +caller too. It is the warning count: + +``` +warnings before: 4 +warnings after 2 authenticated gate runs: 4 +``` + +The pin authenticated the caller and had nothing to warn about. That is the +condition ADHOC-2026-08-17-T01 required before `policy.enabled` may flip +anywhere. + +## What is still open + +`policy.enabled` stays **false**. The remaining sequence (T05) is flex-auth's +move first: `callerAuth.mode: enforce` on this pin (their FLEX-WP-0016 T03), +re-run the gate against the enforcing pin, then `policy.enabled: true` with +`fail_closed: true`, then an end-to-end `warden sign` — which additionally needs +a scoped `VAULT_TOKEN` via `ops-warden-warden-sign-token`. + +Flipping before enforce buys nothing; flipping before this task would have +401'd every `warden sign`. diff --git a/scripts/check_policy_caller_identity.py b/scripts/check_policy_caller_identity.py index dae5f9c..c0a2c2a 100755 --- a/scripts/check_policy_caller_identity.py +++ b/scripts/check_policy_caller_identity.py @@ -97,7 +97,11 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]: import httpx # local import: the offline checks above must not need it probe = { - "subject": {"id": "readiness", "type": "agt", "tenant": policy.tenant}, + "subject": { + "id": "agt-state-hub-bridge", + "type": "agt", + "tenant": policy.tenant, + }, "action": "sign", "resource": { "id": "ssh-cert:actor/agt-state-hub-bridge", @@ -105,7 +109,17 @@ def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]: "system": policy.system, "tenant": policy.tenant, }, - "context": {"readiness_probe": True}, + "context": { + # A structurally complete context, so a deny means the policy said + # no — not that the probe was malformed. What is under test here is + # the caller identity, and that is answered by the HTTP status. + "actor_name": "agt-state-hub-bridge", + "actor_type": "agt", + "principals": ["agt-task-bridge"], + "ttl_hours": 24, + "pubkey_fingerprint": "sha256:" + "0" * 64, + "readiness_probe": True, + }, } try: response = httpx.post( diff --git a/workplans/WARDEN-WP-0031-policy-caller-identity.md b/workplans/WARDEN-WP-0031-policy-caller-identity.md index 798879a..a336409 100644 --- a/workplans/WARDEN-WP-0031-policy-caller-identity.md +++ b/workplans/WARDEN-WP-0031-policy-caller-identity.md @@ -14,6 +14,7 @@ related_workplans: - WARDEN-WP-0009 created: "2026-08-19" updated: "2026-08-19" +state_hub_workstream_id: "7284a398-d529-4fe3-871e-e2505064be5e" --- # WARDEN-WP-0031 — Calling-side identity for flex-auth @@ -63,6 +64,7 @@ point argument, applied to ops-warden as a caller). id: WARDEN-WP-0031-T01 status: done priority: high +state_hub_task_id: "cd65c834-214a-4bbb-92e4-262f124fc79d" ``` **Caller identity on the outbound policy call.** `policy.caller_auth` in @@ -80,6 +82,7 @@ until an operator opts in. Tests in `tests/test_policy.py`. id: WARDEN-WP-0031-T02 status: done priority: high +state_hub_task_id: "9abdeb78-bbbb-4ff5-b3d7-5646116cf68b" ``` **Readiness gate.** `scripts/check_policy_caller_identity.py` — read-only: @@ -96,6 +99,7 @@ Done 2026-08-19. id: WARDEN-WP-0031-T03 status: done priority: medium +state_hub_task_id: "d3ff2ae1-2dac-45d0-b229-8b23c27c3972" ``` **Docs.** `examples/warden.production.example.yaml` gains the `caller_auth` @@ -108,8 +112,9 @@ Done 2026-08-19. ```task id: WARDEN-WP-0031-T04 -status: wait +status: done priority: high +state_hub_task_id: "3d9d1a05-adea-4dcb-b71b-bfb2235aa198" ``` **Pick the token source and prove it against the warn pin.** Operator work on @@ -120,10 +125,22 @@ against a port-forward. Expect `effect=allow` for `agt-state-hub-bridge` while the warn log stops printing `caller authentication warning` — the absence of that line, not the allow, is the evidence. +Done 2026-08-19. Source is `mode: command` — `kubectl create token ops-warden +-n ops-warden --audience flex-auth --duration 10m` against the railiance01 +cluster (tunnel `k3s-api-railiance01`, local `16444`; note `16443` is claimed by +two tunnels at once, which is why a `system:masters` cert 401s there). +`deploy/kubernetes/caller-identity.yaml` creates the Namespace and +ServiceAccount the binding names — no RBAC, `automountServiceAccountToken: +false`; it exists only to be TokenReviewed. Gate exits 0 live: +`HTTP 200, effect=allow, decision:f3f7c88f9585582a`, and the pin's +`caller authentication warning` count held at 4 across two authenticated runs. +Evidence: `history/2026-08-19-flex-auth-caller-identity-evidence.md`. + ```task id: WARDEN-WP-0031-T05 status: wait priority: high +state_hub_task_id: "8b8e68c4-587d-490d-a98f-840aaff0fdb8" ``` **Sequence the flip.** Only after T04: tell flex-auth to set @@ -131,3 +148,7 @@ priority: high re-run the gate against the enforcing pin, and only then set `policy.enabled: true` with `fail_closed: true`. Flipping before enforce buys nothing; flipping before T04 401s every `warden sign`. + +Asked flex-auth 2026-08-19 with the T04 evidence. Waiting on their enforce. +The closing `warden sign` also needs a scoped `VAULT_TOKEN` via +`ops-warden-warden-sign-token` (operator authenticates to OpenBao).