From 0e86f957a217732eb51044a20fe76a35dab435ba Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 11:47:34 +0200 Subject: [PATCH] NK-WP-0048-T05: take ops-warden R8 option 1; ensure-groups attended; memo r4 ensure-groups and its rollback run attended in the register-client session (key-cape-oidc-login), inside the identity-provisioner pod: the provisioner's only HTTP credential is user-engine's runtime token, so a caller-proxy lane would act as user-engine. Lane identity-provisioner-groups is dropped. Memo r4 supersedes r3; acknowledges ops-warden's WARDEN-WP-0044-T01 review. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 469893@bnt-lap001 Assistant-Session: 49296f15-2171-4137-b972-2443bab95edb --- WORK-RECORDS.md | 2 +- .../oidc-browser-relying-party/README.md | 14 +++++---- .../decision/build_intake.py | 7 +++-- .../decision/intake.json | 22 +++++++------- .../decision/packet.md | 30 ++++++++++++------- .../tests/test_oidc_browser_relying_party.py | 7 +++-- .../oidc-browser-relying-party/type.yaml | 19 ++++++++---- workplans/NK-WP-0048-security-case-types.md | 27 ++++++++++++++++- 8 files changed, 88 insertions(+), 40 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index e06a294..66d48a7 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -40,7 +40,7 @@ | workplan | NK-WP-0043 | finished | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | | workplan | NK-WP-0044 | finished | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | | workplan | NK-WP-0045 | finished | — | workplans/NK-WP-0045-root-grant-custody-record.md | -| workplan | NK-WP-0046 | active | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| workplan | NK-WP-0046 | finished | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | | workplan | NK-WP-0047 | finished | — | workplans/NK-WP-0047-platform-password-reset-mail.md | | workplan | NK-WP-0048 | active | — | workplans/NK-WP-0048-security-case-types.md | | task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | diff --git a/security-case-types/oidc-browser-relying-party/README.md b/security-case-types/oidc-browser-relying-party/README.md index e750499..6d7f4b7 100644 --- a/security-case-types/oidc-browser-relying-party/README.md +++ b/security-case-types/oidc-browser-relying-party/README.md @@ -79,7 +79,8 @@ Secret, and agents do not read or write Secrets. For confidential clients, `bind-env` is attended as well, being a change to the identity provider's Deployment. KeyCape resolves a confidential client's secret from an environment variable at start, so the binding (and `deliver-secret` before it) must exist -before registration. ops-warden groups both attended steps into one operator +before registration. `ensure-groups` is attended too (see Lanes below). ops-warden +groups the attended steps into one operator session (WARDEN-WP-0044). The way to remove this step is a KeyCape change: load clients from a non-secret registry and reference confidential secrets from custody. That would be a 1.1 or 2.0 of this type. @@ -87,14 +88,17 @@ confidential secrets from custody. That would be a 1.1 or 2.0 of this type. ## Lanes, value handling and gates Every step ops-warden executes or groups names its catalog lane -(`identity-provisioner-groups` and `keycape-client-secret-generate` are new and -must exist before the first case runs; the attended steps use the existing -`key-cape-oidc-login`). "ops-warden executes" means ops-warden invokes the owner's +(`keycape-client-secret-generate` is new and must exist before the first case +runs; the attended steps use the existing `key-cape-oidc-login`). "ops-warden executes" means ops-warden invokes the owner's entry point through its caller proxy, under the caller's identity: it holds no credential (canon section 3). `mint-secret` generates the value inside the broker and custody (`value_handling: generated_in_custody`), so no value passes through the run. `deliver-secret` gates `bind-env` with a status-only Ready check. -`ensure-groups` adds exactly the memberships recorded in the accepted case. +`ensure-groups` adds exactly the memberships recorded in the accepted case. It is +attended, in the same session as `register-client`, and runs inside the +provisioner pod: the provisioner's only HTTP credential is user-engine's runtime +token, so a caller-proxy lane would act as user-engine (ops-warden review R8). It +can become unattended once the provisioner has an operator credential of its own. ## Known limits diff --git a/security-case-types/oidc-browser-relying-party/decision/build_intake.py b/security-case-types/oidc-browser-relying-party/decision/build_intake.py index 5da50bd..2758c37 100644 --- a/security-case-types/oidc-browser-relying-party/decision/build_intake.py +++ b/security-case-types/oidc-browser-relying-party/decision/build_intake.py @@ -13,10 +13,11 @@ from pathlib import Path HERE = Path(__file__).resolve().parent HUB_DECISION = "e418263e-a71f-4f01-a970-c87c9e187c09" -# r3 supersedes r2 (built before ops-warden's review) and the first memo (built -# before ops-mason's review). Each revision needs a new id: the dispatcher +# r4 supersedes r3 (ensure-groups as a caller-proxy step; ops-warden R8), r2 +# (built before ops-warden's review) and the first memo (built before ops-mason's +# review). Each revision needs a new id: the dispatcher # refuses new content under a delivered idempotency key. -MEMO_ID = "nk-wp-0048-t05-oidc-browser-relying-party-r3" +MEMO_ID = "nk-wp-0048-t05-oidc-browser-relying-party-r4" ITEM = f"{MEMO_ID}-packet" packet = (HERE / "packet.md").read_text() diff --git a/security-case-types/oidc-browser-relying-party/decision/intake.json b/security-case-types/oidc-browser-relying-party/decision/intake.json index 954fe86..3e464c3 100644 --- a/security-case-types/oidc-browser-relying-party/decision/intake.json +++ b/security-case-types/oidc-browser-relying-party/decision/intake.json @@ -1,7 +1,7 @@ { "kind": "informed-decision.memo-intake", "version": 1, - "idempotency_key": "state-hub.e418263e-a71f-4f01-a970-c87c9e187c09.nk-wp-0048-t05-oidc-browser-relying-party-r3", + "idempotency_key": "state-hub.e418263e-a71f-4f01-a970-c87c9e187c09.nk-wp-0048-t05-oidc-browser-relying-party-r4", "memo_class": "decision-record", "review_group": "net-kingdom-admins", "drafted_by": "net-kingdom NK-WP-0048 (claude-code)", @@ -20,7 +20,7 @@ } }, "memo": { - "id": "nk-wp-0048-t05-oidc-browser-relying-party-r3", + "id": "nk-wp-0048-t05-oidc-browser-relying-party-r4", "version": 1, "question": "Acknowledge oidc-browser-relying-party 1.0 as the reusable SecurityCaseType for internal web services that sign humans in through NetKingdom?", "requested_act": "approve", @@ -47,15 +47,15 @@ "step_kind": "approve", "packet": [ { - "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-packet", - "label": "nk-wp-0048-t05-oidc-browser-relying-party-r3-packet", - "hash": "sha256:726dfc010c94d24b95e7de5c1725568531678388c9ef855c7e605d9cf3f50142" + "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-packet", + "label": "nk-wp-0048-t05-oidc-browser-relying-party-r4-packet", + "hash": "sha256:452ea42168ddef64dc5498ba30ae63ff2ff47a3919f55e22f6379d668ea76c28" } ], "highlights": [ { - "id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-h1", - "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-packet", + "id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-h1", + "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-packet", "severity": "critical", "required_ack": true, "locator": { @@ -66,8 +66,8 @@ "note": "Acknowledging changes nothing live and accepts no case. It decides the shape every future browser client case will follow." }, { - "id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-h2", - "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-packet", + "id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-h2", + "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-packet", "severity": "warning", "required_ack": true, "locator": { @@ -85,9 +85,9 @@ }, "packet": [ { - "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r3-packet", + "item_id": "nk-wp-0048-t05-oidc-browser-relying-party-r4-packet", "media_type": "text/markdown", - "content": "# Acknowledge SecurityCaseType oidc-browser-relying-party 1.0\n\nWorkplan: NK-WP-0048 (net-kingdom), task NK-WP-0048-T05\nHub decision: e418263e-a71f-4f01-a970-c87c9e187c09\nCanon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5\nType: security-case-types/oidc-browser-relying-party/ (type.yaml, README.md, tests)\nMemo: nk-wp-0048-t05-oidc-browser-relying-party-r3. It supersedes r2\n(nk-wp-0048-t05-oidc-browser-relying-party-r2, which lacked ops-warden's review)\nand the first memo (already returned). Please return r2 with a comment that r3\nsupersedes it, then answer this one. All three serve the same hub decision, and\nthe first accept or decline resolves it.\n\n## Question\n\nAcknowledge oidc-browser-relying-party 1.0 as the reusable SecurityCaseType for\ninternal web services that sign humans in through NetKingdom?\n\n## One act\n\nMove the type from `proposed` to `acknowledged`. From then on, a browser OIDC\nclient for an internal service is a SecurityCase of this type: its owner fills in\nthe parameters, the case is accepted (owner and founder, risk class medium),\nops-mason materializes it and ops-warden follows through. The type becomes\n`implemented` only after its first case (Forgejo) is live and verified.\n\n## What acknowledging does not do\n\n- It registers no client, creates no group, mints no secret and changes nothing\n live.\n- It does not accept any case. The Forgejo case needs its own acceptance; its\n parameters were accepted in KEY-WP-0038-T01 and carry over, except custody:\n the type puts Forgejo's secret at `platform/workloads/keycape/forgejo`, not\n the request's proposed `platform/workloads/forgejo/netkingdom-oidc-client`.\n- It does not adopt the existing hand-built clients. Adoption, when it happens,\n does not change their live registrations.\n- It does not create the two catalog lanes listed below. They are needed before\n the first case runs (T06), each through its own owner.\n\n## The shape being acknowledged\n\nParameters: client id, display name, exact https redirect URIs (no wildcards),\nscopes from a closed list, client type (confidential or public), MFA pin, the\nLLDAP groups that gate sign-in and their initial members, the consuming service\nand its owner. A case cannot choose where a confidential client's secret lives:\nthe type derives `platform/workloads/keycape/`, as the live\nsecrets-engine-openbao client already uses. Secrets never appear in a case.\n\nSteps, in order, each on its owner's entry point and its catalog lane:\n\n| Step | Owner | Runs | Lane | Values | Confidential only |\n| --- | --- | --- | --- | --- | --- |\n| ensure-groups | net-kingdom (identity-provisioner) | ops-warden | identity-provisioner-groups (new) | none | no |\n| mint-secret | railiance-platform (credential broker) | ops-warden | keycape-client-secret-generate (new) | generated in custody | yes |\n| deliver-secret | railiance-platform (delivery into the sso namespace) | owner; gate: Ready, status only | — | none | yes |\n| bind-env | net-kingdom (KeyCape Deployment) | attended operator | key-cape-oidc-login | none | yes |\n| register-client | net-kingdom (keycape-client.py) | attended operator | key-cape-oidc-login | none | no |\n| hand-over | the consuming service's owner | owner | — | none | no |\n\n\"ops-warden runs\" means exactly this, now written into the canon: ops-warden\ninvokes the owner's entry point through its caller proxy, under the caller's\nidentity and lane. It never holds a standing credential for the step. A secret\nis generated inside the broker and custody and never passes through the run.\n`ensure-groups` adds exactly the memberships recorded in the accepted case and\nnothing else.\n\nops-mason builds, for confidential clients, the custody path; two read policies\nin the existing family, `workload-kv-read-keycape--eso` and\n`workload-kv-read-keycape--`, each granting `read` on\nthat one data path and nothing else; and the Kubernetes-auth role\n`external-secrets-keycape-` that ESO uses to deliver the secret (its\nservice account only, 15m, no default policy). For every case it adds the\ncatalog entry `-keycape-client`, whose body the type templates\n(owner key-cape, risk high, the consumer's workload reference, path and field,\nnative delegation, rotation not automatable until a run is proven). A case adds\nno new path layout, policy family or route design.\n\nVerification never reads a value. The authorization endpoint accepts each\nregistered redirect and refuses another; the groups exist with exactly the\naccepted memberships; for confidential clients, the delivery reports Ready, a\ndefault identity is denied the custody path and the consumer's policy is allowed\nit; when MFA is pinned, the registered client shows the pin. Rollback is the\nexact inverse, in reverse order.\n\n## Why this shape (alternatives rejected)\n\n- Per-client scripts (the status quo): too much manual work, one review surface\n per client.\n- Wildcard or prefix redirect URIs: the classic open-redirect weakness.\n- Automatic accounts for every NetKingdom user: sign-in is gated by explicit\n groups; the consumer decides account creation.\n- Secrets in case files for convenience: cases carry no secrets.\n- A custody path chosen per case: every case would add its own layout and\n policy family to review (rejected in ops-mason's review).\n- One type-level catalog route instead of one entry per case: it would drop the\n consumer's workload reference that zone selection needs (ops-warden withdrew\n this proposal and agreed with ops-mason).\n- ops-warden holding a standing service token: it would become a broker, which\n ops-warden's own design rules out.\n\n## Attended steps\n\n`register-client` (and, for confidential clients, `bind-env`) need one attended\noperator session, grouped by ops-warden on the existing `key-cape-oidc-login`\nlane. KeyCape's live client registry is a Kubernetes Secret, and agents do not\nread or write Secrets. Removing this needs a KeyCape change (clients from a\nnon-secret registry, secrets referenced from custody). That would be a new\nversion of this type, with its own decision.\n\n## Known limits of 1.0\n\nNo plain-http or loopback redirects (openbao-admin, the coulomb.social dev\nURIs), no resource audience, tenant binding or custom scopes\n(informed-decision-approver), no service clients. Each of these is a later\nversion or a separate type. Of the existing hand-built browser clients,\nuser-engine portal and vergabe fit 1.0 exactly.\n\n## Evidence\n\n- Validator: tools/security-case-types/, 22 tests (bodies for policies, roles\n and catalog entries; lanes for executed or attended steps; value handling on\n every step; no generated value in an attended step)\n- This type: 14 tests, including the rendered Forgejo case (custody path, both\n policy bodies, ESO role, catalog entry body, lanes, value handling, the\n delivery gate, the MFA and delivery checks), the Forgejo case checked field by\n field against the accepted request, and a regression showing that the vergabe\n case reproduces its hand-built registration exactly\n- Entry points: identity-provisioner groups ensure/revoke (39 tests),\n keycape-client.py (15 tests)\n- Declaration net-kingdom.keycape-client-registration passes the Playbook\n Capability Contract validator\n\n## Reviews before this decision\n\nBoth executors have reviewed their sections, and everything they required is in\nthis version. ops-mason (MASON-WP-0010-T01) asked for a derived custody path,\nreuse of the existing policy family, templated policy rules, the ESO role and a\nper-case catalog entry. ops-warden (WARDEN-WP-0044-T01) asked for a catalog lane\nper step, a narrow canon meaning of \"ops-warden runs\", per-step value handling,\na Ready gate before bind-env, memberships only from the accepted case, MFA and\ndelivery checks, and a templated catalog entry body. ops-warden and ops-mason\nreport no remaining conflict.\n" + "content": "# Acknowledge SecurityCaseType oidc-browser-relying-party 1.0\n\nWorkplan: NK-WP-0048 (net-kingdom), task NK-WP-0048-T05\nHub decision: e418263e-a71f-4f01-a970-c87c9e187c09\nCanon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5\nType: security-case-types/oidc-browser-relying-party/ (type.yaml, README.md, tests)\nMemo: nk-wp-0048-t05-oidc-browser-relying-party-r4. It supersedes r3 (which\nmade `ensure-groups` an ops-warden caller-proxy step; ops-warden's addendum R8\nshowed the provisioner has no caller credential for that), r2 (which lacked\nops-warden's review) and the first memo (already returned). Please return any\nof r2 and r3 still open with a comment that r4 supersedes them, then answer\nthis one. All memos serve the same hub decision, and the first accept or\ndecline resolves it.\n\n## Question\n\nAcknowledge oidc-browser-relying-party 1.0 as the reusable SecurityCaseType for\ninternal web services that sign humans in through NetKingdom?\n\n## One act\n\nMove the type from `proposed` to `acknowledged`. From then on, a browser OIDC\nclient for an internal service is a SecurityCase of this type: its owner fills in\nthe parameters, the case is accepted (owner and founder, risk class medium),\nops-mason materializes it and ops-warden follows through. The type becomes\n`implemented` only after its first case (Forgejo) is live and verified.\n\n## What acknowledging does not do\n\n- It registers no client, creates no group, mints no secret and changes nothing\n live.\n- It does not accept any case. The Forgejo case needs its own acceptance; its\n parameters were accepted in KEY-WP-0038-T01 and carry over, except custody:\n the type puts Forgejo's secret at `platform/workloads/keycape/forgejo`, not\n the request's proposed `platform/workloads/forgejo/netkingdom-oidc-client`.\n- It does not adopt the existing hand-built clients. Adoption, when it happens,\n does not change their live registrations.\n- It does not create the new catalog lane listed below\n (`keycape-client-secret-generate`, railiance-platform). It is needed before\n the first case runs (T06).\n\n## The shape being acknowledged\n\nParameters: client id, display name, exact https redirect URIs (no wildcards),\nscopes from a closed list, client type (confidential or public), MFA pin, the\nLLDAP groups that gate sign-in and their initial members, the consuming service\nand its owner. A case cannot choose where a confidential client's secret lives:\nthe type derives `platform/workloads/keycape/`, as the live\nsecrets-engine-openbao client already uses. Secrets never appear in a case.\n\nSteps, in order, each on its owner's entry point and its catalog lane:\n\n| Step | Owner | Runs | Lane | Values | Confidential only |\n| --- | --- | --- | --- | --- | --- |\n| ensure-groups | net-kingdom (identity-provisioner, in-pod call) | attended operator | key-cape-oidc-login | none | no |\n| mint-secret | railiance-platform (credential broker) | ops-warden | keycape-client-secret-generate (new) | generated in custody | yes |\n| deliver-secret | railiance-platform (delivery into the sso namespace) | owner; gate: Ready, status only | — | none | yes |\n| bind-env | net-kingdom (KeyCape Deployment) | attended operator | key-cape-oidc-login | none | yes |\n| register-client | net-kingdom (keycape-client.py) | attended operator | key-cape-oidc-login | none | no |\n| hand-over | the consuming service's owner | owner | — | none | no |\n\n\"ops-warden runs\" means exactly this, now written into the canon: ops-warden\ninvokes the owner's entry point through its caller proxy, under the caller's\nidentity and lane. It never holds a standing credential for the step. A secret\nis generated inside the broker and custody and never passes through the run.\n`ensure-groups` adds exactly the memberships recorded in the accepted case and\nnothing else.\n\nops-mason builds, for confidential clients, the custody path; two read policies\nin the existing family, `workload-kv-read-keycape--eso` and\n`workload-kv-read-keycape--`, each granting `read` on\nthat one data path and nothing else; and the Kubernetes-auth role\n`external-secrets-keycape-` that ESO uses to deliver the secret (its\nservice account only, 15m, no default policy). For every case it adds the\ncatalog entry `-keycape-client`, whose body the type templates\n(owner key-cape, risk high, the consumer's workload reference, path and field,\nnative delegation, rotation not automatable until a run is proven). A case adds\nno new path layout, policy family or route design.\n\nVerification never reads a value. The authorization endpoint accepts each\nregistered redirect and refuses another; the groups exist with exactly the\naccepted memberships; for confidential clients, the delivery reports Ready, a\ndefault identity is denied the custody path and the consumer's policy is allowed\nit; when MFA is pinned, the registered client shows the pin. Rollback is the\nexact inverse, in reverse order.\n\n## Why this shape (alternatives rejected)\n\n- Per-client scripts (the status quo): too much manual work, one review surface\n per client.\n- Wildcard or prefix redirect URIs: the classic open-redirect weakness.\n- Automatic accounts for every NetKingdom user: sign-in is gated by explicit\n groups; the consumer decides account creation.\n- Secrets in case files for convenience: cases carry no secrets.\n- A custody path chosen per case: every case would add its own layout and\n policy family to review (rejected in ops-mason's review).\n- One type-level catalog route instead of one entry per case: it would drop the\n consumer's workload reference that zone selection needs (ops-warden withdrew\n this proposal and agreed with ops-mason).\n- ops-warden holding a standing service token: it would become a broker, which\n ops-warden's own design rules out.\n\n## Attended steps\n\n`ensure-groups`, `register-client` (and, for confidential clients, `bind-env`)\nneed one attended operator session, grouped by ops-warden on the existing\n`key-cape-oidc-login` lane. `ensure-groups` is attended because the\nidentity-provisioner's only HTTP credential is user-engine's runtime token and\nits NetworkPolicy admits only user-engine pods: a caller-proxy lane would act as\nuser-engine (ops-warden R8). It runs inside the provisioner pod, and no token is\nread. An operator credential of its own on the provisioner would make it\nunattended in a later version. KeyCape's live client registry is a Kubernetes Secret, and agents do not\nread or write Secrets. Removing this needs a KeyCape change (clients from a\nnon-secret registry, secrets referenced from custody). That would be a new\nversion of this type, with its own decision.\n\n## Known limits of 1.0\n\nNo plain-http or loopback redirects (openbao-admin, the coulomb.social dev\nURIs), no resource audience, tenant binding or custom scopes\n(informed-decision-approver), no service clients. Each of these is a later\nversion or a separate type. Of the existing hand-built browser clients,\nuser-engine portal and vergabe fit 1.0 exactly.\n\n## Evidence\n\n- Validator: tools/security-case-types/, 22 tests (bodies for policies, roles\n and catalog entries; lanes for executed or attended steps; value handling on\n every step; no generated value in an attended step)\n- This type: 14 tests, including the rendered Forgejo case (custody path, both\n policy bodies, ESO role, catalog entry body, lanes, value handling, the\n delivery gate, the MFA and delivery checks), the Forgejo case checked field by\n field against the accepted request, and a regression showing that the vergabe\n case reproduces its hand-built registration exactly\n- Entry points: identity-provisioner groups ensure/revoke (39 tests),\n keycape-client.py (15 tests)\n- Declaration net-kingdom.keycape-client-registration passes the Playbook\n Capability Contract validator\n\n## Reviews before this decision\n\nBoth executors have reviewed their sections, and everything they required is in\nthis version. ops-mason (MASON-WP-0010-T01) asked for a derived custody path,\nreuse of the existing policy family, templated policy rules, the ESO role and a\nper-case catalog entry. ops-warden (WARDEN-WP-0044-T01) asked for a catalog lane\nper step, a narrow canon meaning of \"ops-warden runs\", per-step value handling,\na Ready gate before bind-env, memberships only from the accepted case, MFA and\ndelivery checks, and a templated catalog entry body. ops-warden and ops-mason\nreport no remaining conflict.\n" } ] } diff --git a/security-case-types/oidc-browser-relying-party/decision/packet.md b/security-case-types/oidc-browser-relying-party/decision/packet.md index d993dc7..8e350a4 100644 --- a/security-case-types/oidc-browser-relying-party/decision/packet.md +++ b/security-case-types/oidc-browser-relying-party/decision/packet.md @@ -4,11 +4,13 @@ Workplan: NK-WP-0048 (net-kingdom), task NK-WP-0048-T05 Hub decision: e418263e-a71f-4f01-a970-c87c9e187c09 Canon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5 Type: security-case-types/oidc-browser-relying-party/ (type.yaml, README.md, tests) -Memo: nk-wp-0048-t05-oidc-browser-relying-party-r3. It supersedes r2 -(nk-wp-0048-t05-oidc-browser-relying-party-r2, which lacked ops-warden's review) -and the first memo (already returned). Please return r2 with a comment that r3 -supersedes it, then answer this one. All three serve the same hub decision, and -the first accept or decline resolves it. +Memo: nk-wp-0048-t05-oidc-browser-relying-party-r4. It supersedes r3 (which +made `ensure-groups` an ops-warden caller-proxy step; ops-warden's addendum R8 +showed the provisioner has no caller credential for that), r2 (which lacked +ops-warden's review) and the first memo (already returned). Please return any +of r2 and r3 still open with a comment that r4 supersedes them, then answer +this one. All memos serve the same hub decision, and the first accept or +decline resolves it. ## Question @@ -33,8 +35,9 @@ ops-mason materializes it and ops-warden follows through. The type becomes the request's proposed `platform/workloads/forgejo/netkingdom-oidc-client`. - It does not adopt the existing hand-built clients. Adoption, when it happens, does not change their live registrations. -- It does not create the two catalog lanes listed below. They are needed before - the first case runs (T06), each through its own owner. +- It does not create the new catalog lane listed below + (`keycape-client-secret-generate`, railiance-platform). It is needed before + the first case runs (T06). ## The shape being acknowledged @@ -49,7 +52,7 @@ Steps, in order, each on its owner's entry point and its catalog lane: | Step | Owner | Runs | Lane | Values | Confidential only | | --- | --- | --- | --- | --- | --- | -| ensure-groups | net-kingdom (identity-provisioner) | ops-warden | identity-provisioner-groups (new) | none | no | +| ensure-groups | net-kingdom (identity-provisioner, in-pod call) | attended operator | key-cape-oidc-login | none | no | | mint-secret | railiance-platform (credential broker) | ops-warden | keycape-client-secret-generate (new) | generated in custody | yes | | deliver-secret | railiance-platform (delivery into the sso namespace) | owner; gate: Ready, status only | — | none | yes | | bind-env | net-kingdom (KeyCape Deployment) | attended operator | key-cape-oidc-login | none | yes | @@ -99,9 +102,14 @@ exact inverse, in reverse order. ## Attended steps -`register-client` (and, for confidential clients, `bind-env`) need one attended -operator session, grouped by ops-warden on the existing `key-cape-oidc-login` -lane. KeyCape's live client registry is a Kubernetes Secret, and agents do not +`ensure-groups`, `register-client` (and, for confidential clients, `bind-env`) +need one attended operator session, grouped by ops-warden on the existing +`key-cape-oidc-login` lane. `ensure-groups` is attended because the +identity-provisioner's only HTTP credential is user-engine's runtime token and +its NetworkPolicy admits only user-engine pods: a caller-proxy lane would act as +user-engine (ops-warden R8). It runs inside the provisioner pod, and no token is +read. An operator credential of its own on the provisioner would make it +unattended in a later version. KeyCape's live client registry is a Kubernetes Secret, and agents do not read or write Secrets. Removing this needs a KeyCape change (clients from a non-secret registry, secrets referenced from custody). That would be a new version of this type, with its own decision. diff --git a/security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py b/security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py index d5151a5..5131b99 100644 --- a/security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py +++ b/security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py @@ -125,7 +125,9 @@ def test_confidential_case_renders_custody_delivery_and_one_attended_session(): order = [s["id"] for s in r["follow_through"]] assert order == ["ensure-groups", "mint-secret", "deliver-secret", "bind-env", "register-client", "hand-over"] steps = {s["id"]: s for s in r["follow_through"]} - assert [s["id"] for s in r["follow_through"] if s.get("attended")] == ["bind-env", "register-client"] + attended = [s for s in r["follow_through"] if s.get("attended")] + assert [s["id"] for s in attended] == ["ensure-groups", "bind-env", "register-client"] + assert {s["lane"] for s in attended} == {"key-cape-oidc-login"} assert "keycape-client.py apply" in steps["register-client"]["entry_point"] assert "sso/keycape-forgejo-client" in steps["deliver-secret"]["entry_point"] assert steps["mint-secret"]["entry_point"].endswith("bao:platform/workloads/keycape/forgejo") @@ -140,7 +142,8 @@ def test_ops_warden_review_is_folded_in(): assert msgs(issues) == [] steps = {s["id"]: s for s in r["follow_through"]} lanes = {k: s.get("lane") for k, s in steps.items()} - assert lanes["ensure-groups"] == "identity-provisioner-groups" + assert lanes["ensure-groups"] == lanes["register-client"] + assert steps["ensure-groups"]["attended"] and not steps["ensure-groups"]["warden_executes"] assert lanes["mint-secret"] == "keycape-client-secret-generate" assert lanes["bind-env"] == lanes["register-client"] == "key-cape-oidc-login" assert steps["mint-secret"]["value_handling"] == "generated_in_custody" diff --git a/security-case-types/oidc-browser-relying-party/type.yaml b/security-case-types/oidc-browser-relying-party/type.yaml index 8e9ee2f..efc8c8d 100644 --- a/security-case-types/oidc-browser-relying-party/type.yaml +++ b/security-case-types/oidc-browser-relying-party/type.yaml @@ -156,12 +156,19 @@ spec: follow_through: - id: ensure-groups owner: net-kingdom - entry_point: "identity-provisioner POST /v1/groups/ensure (groups {access_groups}; members exactly {initial_members} as recorded in the accepted case, nothing else)" - auth: "the caller's own identity-provisioner credential through its lane; ops-warden invokes it via the caller proxy and holds no token" - lane: identity-provisioner-groups - warden_executes: true - attended: false + entry_point: "identity-provisioner ensure_service_groups, run inside the provisioner pod through the session's contained cluster access (groups {access_groups}; members exactly {initial_members} as recorded in the accepted case, nothing else)" + auth: "operator cluster access in the attended register-client session; the in-pod call uses the provisioner's own directory binding, and no provisioner or user-engine token is read or sent" + lane: key-cape-oidc-login + warden_executes: false + attended: true value_handling: none + attended_justification: >- + The provisioner's only HTTP credential is user-engine's runtime token, and + its NetworkPolicy admits only user-engine pods. A proxy lane would act as + user-engine (ops-warden review R8; canon section 3; ops-warden ADR-0002). + Running it in the session register-client already needs adds no attended + session. It can become unattended once the provisioner has an operator + credential of its own and an admitted path. - id: mint-secret owner: railiance-platform when: "client_type == confidential" @@ -265,7 +272,7 @@ spec: when: "client_type == confidential" action: "covered by client-secret-path: one metadata delete, after deliver-secret is reversed" - reverses: ensure-groups - action: "identity-provisioner POST /v1/groups/revoke with the case's memberships; delete_if_empty only if no other case uses {access_groups}" + action: "identity-provisioner revoke_service_groups inside the provisioner pod (attended, as ensure-groups) with the case's memberships; delete_if_empty only if no other case uses {access_groups}" - reverses: client-secret-path when: "client_type == confidential" action: "delete metadata and all versions at platform/workloads/keycape/{client_id}, after the ExternalSecret is gone (this also reverses mint-secret)" diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md index 84d2e2a..6903883 100644 --- a/workplans/NK-WP-0048-security-case-types.md +++ b/workplans/NK-WP-0048-security-case-types.md @@ -9,7 +9,7 @@ flavor: implementation owner: claude-code topic_slug: netkingdom created: "2026-10-08" -updated: "2026-10-08" +updated: "2026-10-09" depends_on: [] related_repos: - ops-mason @@ -291,6 +291,31 @@ ops-mason's request to create LLDAP group `fluid-telegram-operators` (message 259ec26d) was answered: not possible from here by that path; the founder creates it in the LLDAP admin UI. +2026-10-09: ops-warden finished its side of WARDEN-WP-0044-T01 (message +`58d33f40`, commit 0e7ceb8): the revised type conforms to `warden case +check-type`, with no further changes asked of the type. This acknowledges that +review. Its later addendum R8 (message `af274783`, ops-warden +`history/2026-10-08-oidc-rp-follow-through-review.md`) showed that lane +`identity-provisioner-groups` cannot exist as declared: the provisioner's only +HTTP credential is user-engine's runtime token, and its NetworkPolicy admits +only user-engine pods, so a caller-proxy lane would act as user-engine. Taken +option 1 (ops-warden's recommendation): `ensure-groups` and its rollback are +attended, in the `register-client` session on `key-cape-oidc-login`, run inside +the provisioner pod; no token is read. Option 2 (an operator credential of its +own on the provisioner) stays open for a later version. `warden case +check-type` now reports only `keycape-client-secret-generate` +(railiance-platform) as missing. 89 tests pass. + +Hub decision `e418263e` was still open, so memo r4 +(`nk-wp-0048-t05-oidc-browser-relying-party-r4`, packet `sha256:452ea421…`) +replaces r3; it passes informed-decision's and the dispatcher's validation. The +founder queues it with the same command, returns r2/r3 if still open with a +comment that r4 supersedes them, then answers r4. + +Prerequisites for T06 are now: lane `keycape-client-secret-generate` +(railiance-platform), and a founder-approved rollout of an identity-provisioner +image containing 630ebfd. + ```task id: NK-WP-0048-T06 status: wait