ops-mason/workplans/MASON-WP-0010-security-case-materialization.md
tegwick 437074dff6 MASON-WP-0009-T05: service-JWT role verified, ops-mason checks complete; pointer proposed
Also confirms the Forgejo ESO role is live as applied (WP-0010).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 327203@bnt-lap001
Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0
2026-10-10 15:51:17 +02:00

230 lines
12 KiB
Markdown

---
id: MASON-WP-0010
type: workplan
title: "Materialize SecurityCases from their type, with acceptance as the only gate"
domain: infotech
repo: ops-mason
status: blocked
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-08"
updated: "2026-10-09"
depends_on: []
related_repos:
- net-kingdom
- ops-warden
- railiance-platform
state_hub_workstream_id: "eabf2c39-94bd-5a11-8f2e-76d7a95d1583"
---
# MASON-WP-0010 — Materialize SecurityCases from their type, with acceptance as the only gate
## Why
NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon
`security-case-types_v0.1`, proposed). A type is designed once, with
documentation, tests and an informed-decision. Each case of an implemented type
is then light: it is accepted, ops-mason arranges it, and ops-warden follows
through.
ops-mason's four phases (construction plan, review, executive summary, build)
stay exactly as they are for **new types and for structure that no type
covers**. For a **KnownSecurityCase**, the construction plan is rendered from
the type's materialization templates, and the executive-summary gate collapses
to the case acceptance, because the shape was decided when the type was
acknowledged.
MASON-WP-0009 (a hand-built placement of a key-cape client secret) is the kind
of work this replaces. It becomes a reference instance for the first type.
## Tasks
```task
id: MASON-WP-0010-T01
status: done
depends_on: [NK-WP-0048-T03]
priority: high
state_hub_task_id: "3382ea7b-65ca-59a9-97ae-273455af44c2"
```
### T01 — Define the type's materialization section
Propose the `materialization` part of the SecurityCaseType schema together with
net-kingdom: policy, role, KV path structure and catalog-entry templates over
case parameters, with naming rules. These must respect ops-mason's existing
"respect, extend or compact before adding" review, so that a type cannot
template sprawl. Write it for the first type, `oidc-browser-relying-party`, from
MASON-WP-0009 and the existing key-cape client custody paths.
**Review (2026-10-08) of net-kingdom `security-case-types/oidc-browser-relying-party/type.yaml` 1.0.**
The structure (four items, all but the catalog entry conditional on
`confidential`) is right. The names add three new families where existing ones
cover the need, so they fail "respect, extend or compact before adding".
Requested refinements, each from the live CCR-2026-0033 placement
(`plans/keycape-secrets-engine-openbao-client-custody.md`):
1. **Fix the path in the type.** `secret_ref` should not be case-settable; a
free pattern under `platform/workloads/` lets every case pick its own layout.
Derive it: `platform/workloads/keycape/{client_id}` (field `CLIENT_SECRET`),
as for `secrets-engine-openbao`.
2. **Reuse the policy family.** Existing readers are
`workload-kv-read-keycape-<client>-<reader>`. Rename
`keycape-client-{client_id}-read` to `workload-kv-read-keycape-{client_id}-eso`
(KeyCape reads its client secret only through ESO), and
`{consuming_service}-oidc-client-{client_id}-read` to
`workload-kv-read-keycape-{client_id}-{consuming_service}`. Update
verification `custody-allowed-to-consumer` and rollback to match.
3. **Template the policy body, not just its name.** An `openbao_policy` item
needs its rules (`read` on `platform/data/workloads/keycape/{client_id}`,
nothing else: no metadata, list, parent or sibling), or the review cannot
check invariant (ii) and a rendered plan is not buildable.
4. **Add the missing ESO role.** `deliver-secret` needs a Kubernetes-auth role
`external-secrets-keycape-{client_id}` (kind `openbao_role`, bound to the
ESO service account, policy from item 2, 15m TTL, no default policy), as
the reference placement has.
5. **Catalog naming.** The one existing entry is `rapp-qonto-keycape-client`;
prefer `{consuming_service}-keycape-client` over `oidc-client-{client_id}`.
6. **Rollback order.** `mint-secret` "destroy" and `client-secret-path`
"remove" act on the same KV path; collapse to one step (metadata delete
after the ExternalSecret is gone) so rollback cannot fail on a missing path.
With 1-4 applied, the section is acceptable to ops-mason for NK-WP-0048-T05.
2026-10-08: net-kingdom folded in all six (net-kingdom `074b3a0`, message
`508e2a12`); its schema/validator now require a body on policy and role items.
NK-WP-0048-T05 still waits on WARDEN-WP-0044-T01 and a founder answer.
2026-10-09: the founder accepted memo r4; hub decision `e418263e` resolved and
the type is `acknowledged` (net-kingdom `0e86f95`). T02 is unblocked.
```task
id: MASON-WP-0010-T02
status: done
depends_on: [MASON-WP-0010-T01, NK-WP-0048-T05]
priority: high
state_hub_task_id: "fd385537-7378-5539-8a6e-f45851296f42"
```
### T02 — Render a construction plan from a case
`mason plan --case <case.yaml>` validates the case against its `implemented`
type, renders the construction plan from the type's templates, and runs the
existing consistency review against current OpenBao structure (read-only, no
values). The output is the usual plan, marked `derived-from-type`, plus the
case-acceptance reference in place of a fresh executive summary.
Tests: render the reference case; refuse a case whose type is not
`implemented`; refuse parameter values outside the type's validation; flag a
collision with existing structure.
2026-10-09 done: `ops-mason plan --case <case.yaml> [--out F] [--no-live]`
(`src/ops_mason/case_plan.py`). Validation and rendering are net-kingdom's own
`security_case_validator.py` (imported, not copied), with provider declarations
loaded through its composer. ops-mason adds the review against existing
structure: a read-only live survey (policy list, role lists, KV metadata,
ops-warden catalog ids) that fails loudly instead of returning an empty
structure, and flags each collision as a finding. Output is a construction
plan with `derived_from_type` and `render_digest`, and section 5 is the case
acceptance; status `reviewed` only when accepted and collision-free. Exit 0
clean, 1 collisions, 2 refused. `tests/test_case_plan.py` (8, on a copy of the
type marked `implemented`); suite 167 passed. The live Forgejo case is refused
today, correctly: the type is `acknowledged`, not yet `implemented`.
2026-10-09: net-kingdom accepted the Forgejo case
(`net-kingdom/security-cases/forgejo-netkingdom-oidc.yaml`, message `d1bbcb44`).
As the reference case it renders with `--allow-unimplemented`, mirroring the
validator's flag: allowed only for an `acknowledged` type, marked
`reference_case: true`; and `reviewed` now requires a live survey. Rendered
live into `plans/case-forgejo-netkingdom-oidc.md`: status `reviewed`, no
collision, digest `sha256:fb8b114d…`.
```task
id: MASON-WP-0010-T03
status: progress
depends_on: [MASON-WP-0010-T02]
blocked_on: message-from:railiance-platform
blocking_reason: "Built 2026-10-09 (builds/case-forgejo-netkingdom-oidc). Acceptance waits on railiance-platform filing the CCR drafts and the founder applying them, plus mint-secret; then the Forgejo case is live."
priority: high
state_hub_task_id: "56cea6be-c613-586c-80cd-125340b374da"
```
### T03 — Build from an accepted case
`mason build --case <case.yaml>` builds a rendered plan when the case's
acceptance record is present, with no separate executive-summary approval. Its
audit trail names the case, the type version and the acceptance. It proposes the
pointer-only ops-warden catalog entries the type defines, as today. Rollback
removes exactly what this case created.
Acceptance: the Forgejo reference case (NK-WP-0048-T06) materializes through this
path.
2026-10-09: implemented. `ops-mason build --case <case> --plan <plan> [--out]
[--allow-unimplemented]` (`src/ops_mason/case_build.py`) refuses unless the
case is accepted, the plan is `reviewed` and its render digest equals a fresh
render. It writes a bundle and never touches OpenBao: one workload-kv-read CCR
draft per policy (ESO policy + Kubernetes role, mirroring CCR-2026-0033; the
consumer policy `in_flight` with `openbao.auth` missing until the consumer owner
binds it), the policy HCL, an ops-warden catalog proposal, `rollback.yaml`
(exactly what the case creates, reverse order) and a manifest with file hashes.
Every file names the case, type version, render digest and acceptance record;
the audit log gets a `case-build-prepared` record with the case acceptance as
approver. The drafts are checked with railiance-platform's own
`credential-change.py validate`. `tests/test_case_build.py` (6); suite 175
passed. Forgejo bundle built into `builds/case-forgejo-netkingdom-oidc/`,
platform validation pass; handed to railiance-platform (message `2c409503`).
```task
id: MASON-WP-0010-T04
status: wait
depends_on: [MASON-WP-0010-T03]
blocking_reason: "After T03, and only once a type fits: secrets-engine-openbao is a service (JWT) client, which oidc-browser-relying-party does not cover."
priority: medium
state_hub_task_id: "f6ad0d3c-8f47-5fdf-8115-6ca9d8a1fead"
```
### T04 — Adopt MASON-WP-0009 as a case
Once `oidc-browser-relying-party` (or the confidential service client type) is
implemented, express MASON-WP-0009's placement as a case of that type without
changing the live structure, and close the hand-built path.
2026-10-10 (T03): railiance-platform filed the drafts as CCR-2026-0041 (ESO
reader) and CCR-2026-0042 (consumer reader, in_flight until railiance-apps binds
it), messages `83465dcd`/`be3a762d`; the founder applied CCR-2026-0041 at
2026-10-09T18:09Z. Its review found a render defect: the CCRs declare
`token_self_lifecycle: true` but the policies lacked the lookup-self/revoke-self
stanzas, so the applier refused them. Fixed in `case_build.py` (test asserts
the three stanzas; output now equals the platform-generated policy apart from
comments) and in the committed bundle. Two follow-ups for the next render:
net-kingdom changed the type to `KEYCAPE_{client_id:env}_CLIENT_SECRET`
(ops-warden `23ba584`), so the render digest will change; and a live re-render
of a partly applied case will report its own objects as collisions. The KV
value waits on mint-secret (RPF-WP-0054).
2026-10-10 (T03 follow-up): `plan --case` now recognises objects listed in the
case's own committed bundle (`builds/case-<id>/rollback.yaml`, same case id) as
"already present from this case's own build", not collisions; anything else
existing still collides (2 tests, suite 177 passed). The committed Forgejo plan
is stale: net-kingdom's `{client_id:env}` change moves the render digest from
`fb8b114d…` to `5e97931e…`. Re-render it live under the next survey grant.
2026-10-10: Forgejo plan re-rendered live: `reviewed`, no collision, digest
`af1f4f81…`; policy `workload-kv-read-keycape-forgejo-eso` shown as already
present from this case's own bundle. The ESO role did not appear as present;
check on the next grant.
2026-10-10: checked. `auth/kubernetes/role/external-secrets-keycape-forgejo`
exists as applied (external-secrets/external-secrets, audience openbao, 15m,
policy `workload-kv-read-keycape-forgejo-eso`, no default policy). It was missing
from the plan's "already present" list because the live survey reads role
lists only for the mount named in the role body, which this run did not
resolve; the collision review is unaffected (it never flagged it).
2026-10-10: type 1.1 (net-kingdom `e2174ce`, message `af66538c`) adds the
consumer-enforces-groups verification and tightens redirect URIs; the case is
re-pinned to @1.1. The live re-render above was already 1.1. Bundle rebuilt
from it (`builds/case-forgejo-netkingdom-oidc/`, platform validation pass);
the ESO policy is unchanged against the applied CCR-2026-0041, so 1.1 changes
no OpenBao object.