diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 5c19c72..58f6f10 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -543,8 +543,8 @@ entries: delegation: mode: interim intended_owner: key-cape - blocked_on: "client_secret_basic exchange is a key-cape protocol procedure, not a KV read; rotation already automatable" - reviewed: "2026-08-11" + blocked_on: "client_secret_basic exchange is a key-cape protocol procedure, not a KV read; KEY-WP-0008 finished 2026-08-15 without a key-cape-native exchange/rotation command, and the workplan that would add one is not yet opened (key-cape acked interim 2026-08-15)" + reviewed: "2026-08-17" risk: high auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange" path_template: "platform/workloads/rapp-qonto/keycape-client" @@ -731,8 +731,13 @@ entries: delegation: mode: interim intended_owner: railiance-apps - blocked_on: "Runtime Secret applied via railiance-apps make target; OpenBao path planned; confirm whether user-engine should front USER_ENGINE_PROXY_SECRET" - reviewed: "2026-08-15" + blocked_on: "Runtime Secret applied via railiance-apps make target; OpenBao path planned but CCR not applied" + reviewed: "2026-08-17" + # USER_ENGINE_PROXY_SECRET ownership settled 2026-08-16 (Bernd; State Hub decision + # 8fe22037-5bbb-4487-bb86-e4beccee454b, USER-WP-0021): it is infrastructure trust + # between ingress and workload, not a user-domain fact. intended_owner stays + # railiance-apps; user-engine is consumer-only and claims no lane here. + consumers: [user-engine] risk: standard # K8s assembly is the live handoff today (same pattern as vergage-teilnahme-env). # OpenBao KV platform/workloads/coulomb/coulomb-social/runtime-env is the diff --git a/wiki/NetKingdomSecurityMap.md b/wiki/NetKingdomSecurityMap.md index 5bf2a0f..31ffa0e 100644 --- a/wiki/NetKingdomSecurityMap.md +++ b/wiki/NetKingdomSecurityMap.md @@ -70,6 +70,86 @@ the need. --- +## Service-to-service caller authentication (in-cluster) + +**Status:** recommended pattern, 2026-08-17. Raised by flex-auth (FLEX-WP-0015 T02): +`POST /v1/check` and `/v1/batch_check` authenticate no caller, so any workload with +network reach can assert any subject and receive an authoritative allow. + +**There is no prior estate pattern for this.** What exists covers adjacent needs and +none of them covers in-cluster service→service HTTP: + +| Existing mechanism | What it authenticates | Why it does not apply | +| --- | --- | --- | +| ops-warden SSH certificates | `adm`/`agt`/`atm` actors to hosts | Host reachability, not an HTTP call between pods | +| KeyCape `client_credentials` | Workload OIDC clients (`rapp-qonto-keycape-client`) | Needs a client secret per caller — a custodied lane per consumer | +| OpenBao AppRole | Host-standing non-interactive workers | `role_id`+`secret_id` on disk; the WP-0030 register already flags AppRole as having **no owner front door** for minting or rotating | + +### Recommendation: Kubernetes ServiceAccount TokenReview + +Use the caller's **projected** ServiceAccount token with an explicit `audience` +(e.g. `flex-auth`); the callee verifies it via `TokenReview`. + +Why this and not the alternatives: + +- **It introduces no new secret material.** A shared-secret header (flex-auth's + option (c)) would immediately become a `risk: high` credential lane with a + rotation owner, per calling system, on the authorization path — precisely the + interim-proxy debt the WP-0030 delegation register exists to stop growing. + ops-warden would end up fronting it. +- **It matches the estate's short-lived-credential doctrine.** A projected, + audience-scoped SA token has the same shape as `warden sign` output: bounded TTL, + issued by an authority, verified on use, never stored. +- **mTLS (option (b)) is the stronger end state but has no owner.** It requires an + X.509 workload CA, and no component owns one today — ops-warden issues SSH + certificates, not workload X.509. Adopting mTLS means first answering *who owns + the workload CA*, which is a permanent-ownership question, not a rollout task. + Record it as future direction; do not block on it. + +Implementation notes that matter: + +- Use a **projected token volume with an explicit `audience`**, not the legacy + automount token. Keep `automountServiceAccountToken: false` and add the projected + volume per Deployment. An audience-scoped token stolen from a pod cannot be + replayed against the kube-apiserver or another service. + +### Authenticate *and* bind — but keep policy in the policy engine + +Bind the `system` asserted in the request to the authenticated ServiceAccount and +reject a mismatch. That is identity binding, not authorization: it is cheap and it +costs nothing extra when a new consumer arrives, since a caller must have a mapping +regardless. + +Do **not** put a caller allowlist for resource types ("only ops-warden may ask about +`ssh-certificate`") in the authentication middleware. flex-auth is the estate's +policy engine; encoding that rule in its own admission layer puts authorization in +two places, where only one of them is reviewable and versioned. Express it in the +policy package. + +### Rollout + +Warn-only first, then fail-closed. ops-warden adopts the calling side on its own +schedule rather than in lockstep. The binding condition is sequencing, not a date: + +```text +flex-auth warn-only -> ops-warden pre-sign gate presents its SA token + -> logs clean of unauthenticated callers + -> flex-auth fail-closed + -> policy.enabled: true (ops-warden side, FLEX-WP-0007) +``` + +`policy.enabled` must not flip anywhere while `/v1/check` still answers +unauthenticated callers. + +**Division of the call:** the mechanism above is an architecture recommendation and +ops-warden's to make. Accepting the pod-spec change and the rollout timing are the +operator's. + +This is a *pattern*, not a credential lane, so it gets no `registry/routing/catalog.yaml` +entry — the catalog indexes credential needs and their owners. + +--- + ## NetKingdom documents to watch | Document | Why ops-warden cares | diff --git a/workplans/ADHOC-2026-08-17.md b/workplans/ADHOC-2026-08-17.md new file mode 100644 index 0000000..2a30e2f --- /dev/null +++ b/workplans/ADHOC-2026-08-17.md @@ -0,0 +1,124 @@ +--- +id: ADHOC-2026-08-17 +type: workplan +title: "Ad Hoc Tasks — 2026-08-17" +domain: infotech +repo: ops-warden +status: finished +owner: claude +topic_slug: custodian +created: "2026-08-17" +updated: "2026-08-17" +--- + +# Ad Hoc Tasks — 2026-08-17 + +Inbox triage session. Three unread messages, all follow-on from the WP-0030 delegation +register plus one new design question that lands on ops-warden as the estate's +workload-identity owner. + +### T01 — Answer flex-auth: how should `/v1/check` authenticate its callers? + +```task +id: ADHOC-2026-08-17-T01 +status: done +priority: high +``` + +flex-auth (msg `130a148c`, FLEX-WP-0015 T02) reported that `POST /v1/check` and +`/v1/batch_check` authenticate no caller — any workload with cluster network reach can +assert any subject/tenant and receive an authoritative allow. It lands on ops-warden +because ops-warden owns how workloads prove identity in this estate, and because the +ops-warden pre-sign gate is a flex-auth PEP that would implement the calling side. + +Their four questions answered in `wiki/NetKingdomSecurityMap.md` +§ *Service-to-service caller authentication (in-cluster)*: + +- [x] **Q1 — is there an existing estate pattern to adopt?** No. Surveyed what exists: + ops-warden SSH certs (host reachability, not pod→pod HTTP), KeyCape + `client_credentials` (`rapp-qonto-keycape-client` — a custodied client secret per + caller), OpenBao AppRole (host-standing; the WP-0030 register already flags it as + having no owner front door). None covers in-cluster service→service HTTP, so + flex-auth is not growing a parallel mechanism by adopting one. +- [x] **Q2 — which mechanism?** Confirmed their instinct: **(a) Kubernetes + ServiceAccount TokenReview**, with a *projected* token carrying an explicit + `audience`, keeping `automountServiceAccountToken: false` and adding the volume + per Deployment. Rejected (c) shared-secret header — it manufactures a `risk: high` + credential lane with a rotation owner per caller, on the authorization path, which + is exactly the interim-proxy debt WP-0030 exists to stop growing, and ops-warden + would end up fronting it. Deferred (b) mTLS as the stronger end state that first + needs an answer to *who owns the workload X.509 CA* — nobody does; ops-warden + issues SSH certs, not workload X.509. +- [x] **Q3 — authenticate only, or also constrain?** Both, but split: bind the asserted + `system` to the authenticated ServiceAccount and reject a mismatch (identity + binding, no per-consumer operational cost). Keep the *resource-type* allowlist + ("only ops-warden may ask about `ssh-certificate`") in the policy package, not in + the auth middleware — flex-auth is the policy engine, and encoding it in its own + admission layer puts authorization in two places where only one is versioned. +- [x] **Q4 — rollout shape?** Warn-only first, as they proposed; ops-warden adopts the + calling side on its own schedule. Binding condition is sequencing, not a date: + `policy.enabled` must not flip anywhere while `/v1/check` still answers + unauthenticated callers. +- [x] Recorded as a **pattern**, not a catalog entry — `registry/routing/catalog.yaml` + indexes credential needs and their owners; caller authentication is neither. +- [x] Noted the division of the call: mechanism is ops-warden's/architectural; accepting + the pod-spec change and rollout timing are the operator's. + +### T02 — user-engine: USER_ENGINE_PROXY_SECRET stays railiance-apps; record consumer-only + +```task +id: ADHOC-2026-08-17-T02 +status: done +priority: medium +``` + +user-engine (msg `2af4a124`) answered the open confirm question on +`coulomb-social-runtime-env`. Decided by Bernd 2026-08-16, State Hub decision +`8fe22037-5bbb-4487-bb86-e4beccee454b` against USER-WP-0021: the secret is +infrastructure trust between ingress and workload, not a user-domain fact; user-engine +consumes it and has no authority over custody, rotation, or issuance. + +- [x] `intended_owner` stays `railiance-apps`; dropped the now-answered + "confirm whether user-engine should front USER_ENGINE_PROXY_SECRET" clause from + `blocked_on`, which now names only the real remaining blocker (CCR not applied). +- [x] Recorded the positive signal they asked for: `consumers: [user-engine]` on the + lane, with the decision id in a comment. +- [x] Their second point — `USER_ENGINE_EVENT_TOKEN`, `USER_ENGINE_MAIL_TOKEN`, and the + transactional SMTP lane gating public registration — checked against the catalog: + `audit-core-senders` (native → ops-mason) and `email-connect-transactional` + (interim → secrets-engine) already cover the event and mail token custody. No new + lane opened; told them which ids to watch. + +### T03 — key-cape: `rapp-qonto-keycape-client` interim accepted; refresh the blocker + +```task +id: ADHOC-2026-08-17-T03 +status: done +priority: medium +``` + +key-cape (msg `099b7cba`) acknowledged the lane staying interim on ops-warden. +KEY-WP-0008 is finished and its closeout does **not** include a key-cape-native +`client_secret_basic` exchange/rotation command; that is a separate, not-yet-opened +workplan. + +- [x] Refreshed `blocked_on` so it names the current reality rather than implying an + in-flight KEY-WP-0008 dependency: the native command is unopened work, and + key-cape has acked the interim hold. +- [x] `key-cape-oidc-login` left pointing at `secrets-engine` — key-cape explicitly did + not claim the generic-vs-per-lane custody split, and that lane's blocker is the + same unanswered secrets-engine question as the other six. +- [x] `reviewed` bumped to 2026-08-17 on both touched lanes so `warden route gaps` + staleness reflects a real re-check. + +### T04 — Session hygiene + +```task +id: ADHOC-2026-08-17-T04 +status: done +priority: low +``` + +- [x] `uv run pytest -q` → 338 passed, 4 deselected (routing anchor + no-double-source + checks green after the catalog edits). +- [x] All three messages replied to and marked read; inbox empty.