diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/ADHOC-2026-09-27.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/ADHOC-2026-09-27.md new file mode 100644 index 00000000..0b755547 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/ADHOC-2026-09-27.md @@ -0,0 +1,100 @@ +--- +id: ADHOC-2026-09-27 +type: workplan +title: "Fresh MFA no longer backdated to a stale kc_login cookie (INFD-IN-0005)" +domain: infotech +repo: key-cape +status: finished +owner: claude +created: "2026-09-27" +updated: "2026-09-27" +state_hub_workstream_id: "5217dd06-73cb-50f8-ba64-528e421b29de" +--- + +## Stop completeAuthorization from reusing a stale prior session's IssuedAt after real MFA + +```task +id: ADHOC-2026-09-27-T01 +status: done +priority: high +state_hub_task_id: "ce98e4af-cfaa-5620-8f21-15ade18ee9e8" +``` + +`informed-decision` intake `INFD-IN-0005` (open, `origin_ref: SECRETS-WP-0010`): +platform-root's approval of `infd-20260927-b01` was refused by flex-auth's +`compact_sitting` policy with `policy_denied`, despite platform-root having just +completed a fresh key-cape MFA sign-in. flex-auth's 900-second review-freshness +check reads `assurance.at`, which key-cape derives from the PKCE session's +`AuthTime`. + +Root cause in `src/internal/server/oidc/authorize.go`, +`AuthorizeHandler.completeAuthorization`: whenever a `kc_login` cookie already +existed for the same username, its `IssuedAt` was unconditionally reused as +`authTime` — even on the two call sites that had just called +`MFA.ValidateMFAToken` successfully in this exact request (the callback's +`mfaToken != ""` branch and `serveMFASubmission`). A user who had an older +browser session and then stepped up with a fresh MFA challenge got the *old* +session's timestamp stamped onto the *new*, just-proven authentication — +understating freshness for every downstream binding-grade check, exactly as +`INFD-IN-0005` describes ("completeAuthorization currently copies a prior +timestamp... only after actual reauthentication"). + +The reuse itself is correct when MFA was **not** just performed in this +request (`decision.MFAVerified` carried forward from an existing AAL2 session, +via `domain.DecideAssurance`) — reporting mint time there would overstate +freshness (`TestAssuranceReportsAuthenticationTimeNotMintTime`). The two cases +were conflated under a single `mfaVerified` bool. + +Fix: added a `freshMFA bool` parameter to `completeAuthorization`, `true` only +at the two call sites that just validated an MFA token in this request, `false` +at the carried-forward-assurance call site. `authTime` now falls back to the +prior session's `IssuedAt` only when `!freshMFA`. + +Added `TestAuthorizeCallback_MFASubmission_FreshEvenWithStalePriorLoginCookie`, +which seeds a 4-hour-old `kc_login` cookie for the same user, then submits a +valid MFA token and asserts the resulting `AuthTime` is within a minute of now. +Confirmed it fails against the pre-fix code (`AuthTime` ~4h old) and passes +after. Full suite (`go test ./...`) green, no other test needed adjustment — +the carried-forward-assurance case (`TestPolicy_...`, `TestLogout_...`, +`assurance_time_test.go`) is unaffected since none of it exercises a fresh MFA +submission with a pre-existing cookie for the same user. + +Does not touch flex-auth (its 900s window is correct; it was consuming a bad +timestamp) or informed-decision (correctly refuses to interpret the PDP's +denial). `INFD-IN-0005` can close once informed-decision confirms platform-root +can approve after a fresh sign-in against this build. + +## Discovery/JWKS fetch was missing the split-horizon origin headers + +```task +id: ADHOC-2026-09-27-T02 +status: done +priority: high +``` + +Rolling out T01 (image digest `sha256:248e449a...`) broke every KeyCape login, +including unrelated clients (`user-engine-portal`), with `auth_failure` / +`provider_metadata_unavailable` at `/api/oidc/token` on the KeyCape side and +Authelia logging `invalid X-Forwarded-Proto header value 'http'` on +`GET /.well-known/openid-configuration` from KeyCape's pod IP. + +Root cause, unrelated to T01's change: `AutheliaAdapter.setPublicOriginHeaders` +(the NK-WP-0041-T02/path-B fix, key-cape@3b0446e) annotates only the token +exchange (`newTokenRequest`) with `X-Forwarded-Proto`/`X-Forwarded-Host` so +Authelia can derive the public issuer for a split-horizon (in-cluster HTTP, +public HTTPS) deployment. `idTokenVerifier.get` in `verify.go` — used for the +provider-metadata and JWKS fetches that verify every upstream ID token — never +got the same treatment; it built a bare request with only an `Accept` header. +Authelia 4.38 fails closed on the resulting unmarked in-cluster request. This +gap predates today's session (present since 3b0446e); it surfaced now because +this was the first login attempted since that image went live. + +Fix: extracted `setPublicOriginHeaders` from an `AutheliaAdapter` method to a +package-level function taking the target public base URL explicitly, and gave +`idTokenVerifier` its own resolved `publicBaseURL` (same +`BrowserBaseURL`-else-`BaseURL` fallback as the adapter) so `get()` applies +the same headers. Added `TestDiscoveryFetch_CarriesPublicOriginHeaders` +(confirmed it fails without the fix, passes with it). Full suite green. + +New image `key-cape@` still needs a rollout to `sso-mfa/k8s/keycape/deployment.yaml` +before platform-root can sign in again. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md new file mode 100644 index 00000000..74334411 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md @@ -0,0 +1,499 @@ +--- +id: KEY-WP-0001 +type: workplan +title: "KeyCape Implementation — Lightweight IAM Profile" +domain: infotech +repo: key-cape +status: finished +owner: Bernd +topic_slug: netkingdom +topic_id: a6c6e745-bf54-4465-9340-1534a2be493e +repo_id: 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c +created: 2026-03-13 +updated: 2026-03-13 +spec_refs: + - wiki/KeyCapeSpecification_v0.1.md + - wiki/KeyCapeSpecificationPack_v0.1.md +decisions: + - id: ADR-0001 + title: "Implementation language: Go" + hub_decision_id: 620beb04-fa3f-4a9d-9806-02890a7a2b0d + status: accepted + ref: docs/adr/ADR-0001-choose-go-for-keycape.md +state_hub_workstream_id: "0d34dfc1-7ccb-4bd5-b872-5c7379b9adce" +--- + +# KEY-WP-0001 — KeyCape Implementation + +Implements the **NetKingdom IAM Profile** via KeyCape: a stateless, profile-constrained OIDC +server orchestrating Authelia, LLDAP, and privacyIDEA. Replaceable by Keycloak without +application changes. + +## Language Decision + +**Go** — decided 2026-03-13 by Bernd. See `docs/adr/ADR-0001-choose-go-for-keycape.md`. + +KeyCape is an orchestrating boundary service (HTTP, adapters, JWT via library, CLI tooling) — +Go's strongest domain. Rust revisitable if a subcomponent needs stronger guarantees later. +Mandatory guardrails: typed domain models, narrow adapter interfaces, layered architecture, +fuzz tests on validator/redirect/claim-mapping, no cleverness. + +## Repo Structure (from spec §12) + +``` +src/ + server/ + oidc/ # Profile endpoints + telemetry/ # Structured event emission + errors/ # Error taxonomy + enforcement middleware + adapters/ + authelia/ # Auth flow delegation + lldap/ # Identity directory reads + privacyidea/ # MFA enforcement + validator/ # Canonical LDAP schema validator binary + migration/ + lldap-export/ # LLDAP → canonical + keycape-to-keycloak/ # Canonical → Keycloak realm import + lldap-to-ldap/ # LLDAP → OpenLDAP/389DS/AD LDIF +spec/ + canonical-model.yaml # Source of truth for all identity data + ldap-schema.yaml # Canonical LDAP schema rules +tests/ + profile/ # Scenario A — lightweight baseline + negative/ # Scenario D — unsupported feature rejection + migration/ # Scenarios B & C — replacement +``` + +## Dependency Order + +``` +T01 (project setup) + └─ T02 (canonical model) T04 (error taxonomy) + └─ T03 (LDAP validator) └─ T13 (telemetry) + └─ T10 (LLDAP adapter) └─ T14 (enforcement layer) + └─ T11 (Authelia) │ + └─ T12 (privacyIDEA) │ + │ │ + T05 ─ T06 ─ T07 ─ T08 ─┴─ T09 (OIDC server) + │ + T18 (profile tests / Scenario A) + T21 (negative tests / Scenario D) + │ + T15 → T16 → T19 (Scenario B) + T15 → T17 → T20 (Scenario C) + │ + T22 (dev stack) + T23 (production packaging) +``` + +--- + +## Phase 1 — Foundations + +## T01 — Project setup: Go module, repo layout, CI skeleton + +```task +id: KEY-WP-0001-T01 +hub_task_id: 25613e3f-2a65-409e-afaa-d23ded0bc256 +priority: high +status: done +state_hub_task_id: "38822bc0-4189-4909-874e-ea40e5771250" +``` + +Initialise language module in `src/`. Create directory skeleton per spec §12. Add Makefile +targets: `build`, `test`, `lint`. Set up CI (build + test on every push). Scaffolding only — +no application code. **Agent must call `record_decision()` with chosen language (Go or Rust).** + +## T02 — Canonical identity model: machine-readable schema + +```task +id: KEY-WP-0001-T02 +hub_task_id: deee2929-9386-41db-bf91-fbd9ad646c28 +priority: high +status: done +depends_on: [T01] +state_hub_task_id: "940c118b-c1e6-4dda-bd4c-4fac105822be" +``` + +Write `spec/canonical-model.yaml`. Six entities: User, Group, Role, Client, Membership, +MFAEnrollment (fields per spec §2). Include JSON Schema or CUE schema for programmatic +validation. This file is the **source of truth** — all other code derives from it. + +## T03 — Canonical LDAP schema + validator + +```task +id: KEY-WP-0001-T03 +hub_task_id: 02592c65-db23-474b-b06b-019e95df8146 +priority: high +status: done +depends_on: [T01, T02] +state_hub_task_id: "c1715d70-f10f-45e9-b73a-b54a3d360342" +``` + +Write `spec/ldap-schema.yaml`: tree layout (`ou=users`, `ou=groups`, `ou=clients` under +`dc=netkingdom,dc=local`), object classes (`inetOrgPerson`, `groupOfNames`), required/optional +attributes. Implement `validator/` binary. Structural rules: valid DN, required attrs, no unknown +attrs, valid group memberships. Semantic rules: referenced users exist, no cycles, usernames +unique, email format valid. Validator runs in `--mode=ci|provisioning|migration`. Emits +machine-readable report. + +## T04 — Error taxonomy: types, JSON format, middleware + +```task +id: KEY-WP-0001-T04 +hub_task_id: 46870fd6-0672-432b-8824-6bc2e24811b3 +priority: high +status: done +depends_on: [T01] +state_hub_task_id: "6e3b6b97-ac77-44c5-959e-be12751f1b63" +``` + +Implement four error types (spec §5): +- `feature_not_supported_by_profile` +- `available_in_keycloak_mode_only` +- `rejected_for_profile_safety` +- `invalid_profile_usage` + +JSON format: `{"error": "...", "description": "...", "feature": "..."}`. HTTP middleware wraps +all handler errors. Error type strings are stable and test-assertable. + +--- + +## Phase 2 — OIDC Server + +## T05 — OIDC discovery endpoint (/.well-known/openid-configuration) + +```task +id: KEY-WP-0001-T05 +hub_task_id: 92eb8916-cb22-4786-9f16-a8a07272f818 +priority: high +status: done +depends_on: [T04] +state_hub_task_id: "0dbc08e3-c465-4c37-a219-832a580bedfd" +``` + +`GET /.well-known/openid-configuration`. Advertise **only** profile-supported features: +`authorization_code`, S256 PKCE, RS256, static scopes. Must NOT advertise: dynamic registration, +implicit flow. Issuer configurable. Cacheable response. + +## T06 — Authorization endpoint with PKCE and redirect URI validation + +```task +id: KEY-WP-0001-T06 +hub_task_id: c3df620b-9864-4ff1-ba6d-26057c6f4d59 +priority: high +status: done +depends_on: [T04, T11, T12, T13, T14] +state_hub_task_id: "cdb4b06d-3d54-49dd-ac05-ca9ed6d7322f" +``` + +`GET/POST /authorize`. Validate: `client_id` (static config), `redirect_uri` (exact match — +wildcard → `rejected_for_profile_safety`), `response_type=code`, `scope` contains `openid`, +`code_challenge` present (missing → `invalid_profile_usage`), `code_challenge_method=S256`. +Delegate to Authelia adapter. Store PKCE state server-side. No implicit or hybrid flow. + +## T07 — Token endpoint: JWT/RS256, canonical claim mapping + +```task +id: KEY-WP-0001-T07 +hub_task_id: d3248f4d-e0e9-4144-9844-c9768dc896d6 +priority: high +status: done +depends_on: [T06, T08, T10] +state_hub_task_id: "534d8616-90de-4d32-961c-c2ef719642e4" +``` + +`POST /token`. Validate PKCE `code_verifier`. Issue RS256 JWT via standard library (no custom +crypto). Mandatory claims: `iss`, `sub` (canonical user ID), `aud`, `exp`, `iat`. Optional +claims: `preferred_username` (LDAP `uid`), `email` (LDAP `mail`), `groups` (groupOfNames), +`roles`. Short, explicitly configured token lifetime. Any other grant type → +`feature_not_supported_by_profile`. + +## T08 — JWKS endpoint (/jwks) + +```task +id: KEY-WP-0001-T08 +hub_task_id: 58a1d705-b788-4d66-8c0a-33edff63a885 +priority: high +status: done +depends_on: [T01] +state_hub_task_id: "7e2167be-bcc7-49c2-8681-e518abd5bc0c" +``` + +`GET /jwks`. RS256 public key in JWK Set format. Key loaded from config. Key rotation: serve +multiple keys during rotation window, keyed by `kid`. Standard library key generation only. + +## T09 — Userinfo endpoint (/userinfo) + +```task +id: KEY-WP-0001-T09 +hub_task_id: 742d3924-21e9-4304-86e9-0400af0e81ee +priority: medium +status: done +depends_on: [T07, T10] +state_hub_task_id: "78094ca5-a831-4443-9ccf-fc476ff87b91" +``` + +`GET /userinfo`. Optional per spec — implement if any registered client requires it. Validate +Bearer token (RS256 + expiry). Return claim subset scoped to granted scopes. Claims must be +identical to ID token for same scopes. If no client needs it: stub returning +`available_in_keycloak_mode_only`. + +--- + +## Phase 3 — Backend Adapters + +## T10 — LLDAP adapter: user and group reads + +```task +id: KEY-WP-0001-T10 +hub_task_id: 2043b10a-6822-45f8-abcc-4e233d918fb0 +priority: high +status: done +depends_on: [T02, T03] +state_hub_task_id: "97d19662-f482-4ea5-84fd-9fccb84ff317" +``` + +`adapters/lldap`. LDAP protocol connection to LLDAP. Interface: `LookupUser(username) → canonical +User`, `LookupGroups(userDN) → []Group`, `ValidatePassword(username, password) → bool`. Attribute +map: `uid→username`, `cn→displayName`, `mail→email`, `memberOf→groups`. Run canonical LDAP schema +validator on every read. No LDAP internals exposed to `server/`. + +## T11 — Authelia adapter: session and auth flow delegation + +```task +id: KEY-WP-0001-T11 +hub_task_id: ad129a14-1552-4717-b1dd-b529d18ce681 +priority: high +status: done +depends_on: [T04, T13] +state_hub_task_id: "6461865b-f57c-4591-9cf3-68c79af22723" +``` + +`adapters/authelia`. Initiate auth redirect to Authelia, receive callback, extract authenticated +user identity, hand off to MFA check (T12). Must not leak Authelia session tokens/cookies into +profile layer. Unavailable Authelia → fail closed (`auth_failure` event). + +## T12 — privacyIDEA adapter: MFA enforcement + +```task +id: KEY-WP-0001-T12 +hub_task_id: 1ef196e6-2304-4cf6-b205-47ac1da879ec +priority: high +status: done +depends_on: [T02, T13] +state_hub_task_id: "e403a783-c856-4d6d-b859-a9cad7545fe1" +``` + +`adapters/privacyidea`. **KeyCape must NOT implement MFA logic.** Interface: +`CheckMFARequired(user) → bool`, `ValidateMFAToken(user, token) → bool`. MFA failure → no token +issued + `auth_failure` telemetry. MFA enrollment state from canonical `MFAEnrollment` entity. +privacyIDEA remains stable across lightweight → expanded migration. + +--- + +## Phase 4 — Telemetry & Enforcement + +## T13 — Telemetry pipeline: structured event emission + +```task +id: KEY-WP-0001-T13 +hub_task_id: 704146bf-cd60-4922-b18b-3d209cff3ac3 +priority: high +status: done +depends_on: [T01] +state_hub_task_id: "4df7bda1-5b84-4b4c-9b16-bcb1d3cca096" +``` + +`server/telemetry`. Event types (spec §6.1): `auth_start`, `auth_success`, `auth_failure`, +`token_issued`, `unsupported_feature`, `invalid_request`, `migration_event`. Required fields +(spec §6.2): `timestamp`, `client_id`, `endpoint`, `feature`, `result`, `error_type`, `scopes`, +`grant_type`, `environment`, `trace_id`. Pluggable outputs: structured log (default), Prometheus +metrics endpoint. Every auth and error path emits an event — **no silent paths**. + +## T14 — Unsupported feature enforcement layer + +```task +id: KEY-WP-0001-T14 +hub_task_id: 71f44886-ab61-4160-a435-72b35af472a0 +priority: high +status: done +depends_on: [T04, T13] +state_hub_task_id: "ae16fba9-5bb4-4780-ac77-558e3ed7e1dd" +``` + +`server/errors` enforcement middleware. Intercept any parameter, grant type, scope, or client +config exceeding the profile. Return correct error type + emit `unsupported_feature` telemetry. +Maintain a **registry** of unsupported features (adding new ones requires no handler changes): +`dynamic_client_registration`, `identity_broker`, `wildcard_redirect_uri`, `implicit_flow`, etc. +Every registry entry must have a corresponding test in T21. + +--- + +## Phase 5 — Migration Tooling + +## T15 — Migration: LLDAP → canonical export + +```task +id: KEY-WP-0001-T15 +hub_task_id: 1bd13f76-2d62-429d-b230-d785ef6a3f2f +priority: medium +status: done +depends_on: [T02, T03, T10] +state_hub_task_id: "f7549cd7-33f0-4407-a656-ab8f5a184e64" +``` + +`migration/lldap-export` tool. Read all users, groups, memberships, attributes from LLDAP. Map +to canonical model. Validate against LDAP schema validator before writing. Output: +`canonical-export.yaml`. Emit `migration_event` telemetry. Idempotent. Include incompatibility +report for unmappable LLDAP data. + +## T16 — Migration: canonical → Keycloak import + +```task +id: KEY-WP-0001-T16 +hub_task_id: f3d50e80-f6b5-4c0c-a5f3-08308ea1a95e +priority: medium +status: done +depends_on: [T15] +state_hub_task_id: "96486c41-9f33-42a5-b7b6-ad0a9eb2bdee" +``` + +`migration/keycape-to-keycloak` tool. Read canonical export (T15). Transform to Keycloak realm +import format: users, groups, clients, roles, scope mappings. Preserve: same issuer, same claims, +same scopes, same client behavior. Output: `keycloak-realm-import.json`. Emit `migration_event`. +Include round-trip validation report. + +## T17 — Migration: LLDAP → full LDAP (OpenLDAP / 389DS / AD) + +```task +id: KEY-WP-0001-T17 +hub_task_id: 044d99c8-39cb-4f35-9308-912ae829bd22 +priority: medium +status: done +depends_on: [T15] +state_hub_task_id: "1ec335a2-80ca-4c34-b08e-211f537e4214" +``` + +`migration/lldap-to-ldap` tool. Export via T15 canonical export. Generate LDIF for target +(`--target=openldap|389ds|ad`). Migrate: users, groups, memberships, attributes. Run validator +on output LDIF before import. Produce validation report. **Orthogonal to T16** — the two +migration dimensions are independent (spec §14.1). + +--- + +## Phase 6 — Replacement Tests + +## T18 — Profile test suite: Scenario A (lightweight baseline) + +```task +id: KEY-WP-0001-T18 +hub_task_id: 76abc3f6-9c4e-4aca-9995-72b728925812 +priority: high +status: done +depends_on: [T05, T06, T07, T08, T09, T22] +state_hub_task_id: "1b0e9f26-d441-42b8-b532-1eb713fb355d" +``` + +`tests/profile`. Provision canonical fixtures into LLDAP + Authelia + KeyCape. Test categories +(spec §15.3): discovery, login flow (PKCE), token claim assertions (all mandatory + optional), +redirect validation, client config, MFA policy, logout (if implemented). Tests are +**backend-agnostic** — same suite runs in T19 and T20. Must pass for Scenario A conformance. + +## T19 — Replacement test suite: Scenario B (IAM swap, same directory) + +```task +id: KEY-WP-0001-T19 +hub_task_id: 56d03e89-934b-4992-bfe4-b32f275882e3 +priority: medium +status: done +depends_on: [T18, T16] +state_hub_task_id: "a02d24e7-32de-4be6-935c-896c10dde020" +``` + +Run T18 suite against Keycloak + LLDAP (configured from T16 canonical export). **No test code +changes allowed.** Migration successful only if all T18 tests pass. Proves IAM replaceability +without directory migration. + +## T20 — Replacement test suite: Scenario C (full expansion) + +```task +id: KEY-WP-0001-T20 +hub_task_id: ec3cae5c-9942-4be7-acc0-1eb9f02aba45 +priority: medium +status: done +depends_on: [T19, T17] +state_hub_task_id: "545f319f-053d-48bd-8d94-c8c05cd56736" +``` + +Apply T17 LLDAP→OpenLDAP migration, then T16 Keycloak import. Run T18 suite. Migration successful +only if all tests pass. privacyIDEA must remain stable (no MFA re-enrollment required). + +## T21 — Negative profile tests: Scenario D (unsupported feature rejection) + +```task +id: KEY-WP-0001-T21 +hub_task_id: a5112b63-121f-4d17-ac1a-fb46d160413e +priority: high +status: done +depends_on: [T14] +state_hub_task_id: "5856afe0-2a9e-4489-b057-35e59f86c359" +``` + +`tests/negative`. For every entry in T14 unsupported-feature registry: attempt usage, assert +correct `error.error` string, assert `unsupported_feature` telemetry event emitted. Covered +cases: `dynamic_client_registration`, `implicit_flow`, wildcard redirect URIs, identity brokering, +missing PKCE, unknown scopes, unknown grant types. **Must run in CI.** Proves enforcement layer +is complete. + +--- + +## Phase 7 — Operations + +## T22 — Dev mode stack: docker-compose with LLDAP + Authelia + +```task +id: KEY-WP-0001-T22 +hub_task_id: e840963f-cd19-4b38-857a-7c40df165d3d +priority: medium +status: done +depends_on: [T01] +state_hub_task_id: "b98f2671-a20a-4438-99c9-fbe0e5324534" +``` + +`docker-compose.dev.yml`: KeyCape, LLDAP, Authelia, privacyIDEA (or stub). Pre-seeded with +canonical fixtures from T02. Makefile targets: `make dev` (start + verify basic auth flow), +`make seed` (re-apply fixtures without full restart). Deterministic: same seed → same state. +Test environment for T18 and T21. + +## T23 — Slim production packaging: binary + config + health + +```task +id: KEY-WP-0001-T23 +hub_task_id: d5723683-f739-4362-b62e-71213dc5a89e +priority: low +status: done +depends_on: [T18, T21] +state_hub_task_id: "8c1752c2-7fb3-4da5-aab3-6b7acf12ea64" +``` + +Single stateless binary. Declarative YAML config: profile version, client definitions, backend +connections, LDAP schema rules, privacyIDEA settings, telemetry destination, token lifetime, +issuer URL. Static config validation on startup. `/healthz` endpoint. Minimal container image +(distroless or Alpine). Config environment-promotable via env var overrides only. + +--- + +## Acceptance Criteria (from spec §15.4 and §20) + +A release is conformant when: + +- [x] Scenario A tests pass (T18) — `src/tests/profile/profile_test.go` (8 tests) +- [x] Scenario D tests pass (T21) — `src/tests/negative/negative_test.go` (8 tests) +- [x] Scenario B tests pass (T19) — `src/tests/migration/scenario_b_test.go` (7 tests) +- [x] Scenario C tests pass (T20) — `src/tests/migration/scenario_c_test.go` (6 tests) +- [x] All error responses use taxonomy types from spec §5 — `internal/errors/taxonomy.go` +- [x] All auth/error paths emit structured telemetry (T13) — `internal/server/telemetry/` +- [x] Canonical LDAP schema validator passes on all fixtures (T03) — `internal/validator/` +- [x] No handwritten cryptography anywhere in the codebase — stdlib `crypto/rsa` only +- [x] Config is statically validated at startup (T23) — `internal/config/validate.go` diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md new file mode 100644 index 00000000..418f477c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md @@ -0,0 +1,245 @@ +--- +id: KEY-WP-0002 +type: workplan +title: "KeyCape Container Image — Build & Publish to Gitea OCI Registry" +domain: infotech +repo: key-cape +status: finished +owner: netkingdom +topic_slug: netkingdom +created: "2026-03-22" +updated: "2026-03-21" +capability_request_id: "" +state_hub_workstream_id: "c8843c7a-460a-47a2-b45a-b8d3940f9aa2" +--- + +# KEY-WP-0002 — KeyCape Container Image — Build & Publish to Gitea OCI Registry + +## Problem + +KeyCape has a `Dockerfile` but no automated build pipeline and no published +image. Other services (k3s deployments, local dev) that need to run KeyCape +must build locally from source. There is no versioned artefact to reference +in Helm charts or manifests. + +The capability request for this work was originally misrouted to railiance. +It belongs here: KeyCape owns its own image. + +## Goal + +Produce a versioned OCI image for KeyCape, published to the Gitea container +registry on CoulombCore (`92.205.130.254:32166`), triggered automatically on +every merge to `main` and on semver tags (`v*`). + +**Gitea OCI registry endpoint:** `92.205.130.254:32166` +**Image name:** `92.205.130.254:32166/coulomb/key-cape` + +> **Why Gitea, not GHCR?** +> The net-kingdom cluster is self-hosted. Keeping images in Gitea (also +> self-hosted on CoulombCore) avoids any external registry dependency and +> keeps image pulls within the cluster network. GHCR is a future option +> once public distribution is needed. + +## Design + +### Image naming & tagging + +| Trigger | Tags applied | +|---------|-------------| +| push to `main` | `latest`, `main-` | +| tag `v1.2.3` | `1.2.3`, `1.2`, `1`, `latest` | + +### Build + +Multi-stage Dockerfile already present — no changes needed to the build +itself. The image builds to a distroless static binary (~10 MB). + +### Registry auth + +Gitea issues a personal access token (or machine account token) with +`write:packages` scope. Stored as Gitea Actions secret `REGISTRY_TOKEN`; +username stored as `REGISTRY_USER`. + +For local `make push`, credentials are passed via `docker login` before +the push target runs. + +### Makefile targets + +```makefile +IMAGE_REGISTRY ?= 92.205.130.254:32166 +IMAGE_REPO ?= coulomb/key-cape +IMAGE_TAG ?= latest +IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG) + +image: + docker build -t $(IMAGE) . + +push: image + docker push $(IMAGE) + +image-tag: + docker tag $(IMAGE) $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG) +``` + +### Gitea Actions workflow + +`.gitea/workflows/image.yaml` — triggers on push to `main` and on `v*` tags: +- Checkout +- Set up Docker Buildx +- Login to `92.205.130.254:32166` using secrets +- Build and push with metadata-action tags +- (Optional) sign with cosign if available + +### k3s insecure registry + +Gitea runs over plain HTTP on port 32166 (NodePort). k3s must be configured +to treat this endpoint as an insecure registry so image pulls work from +within the cluster: + +```yaml +# /etc/rancher/k3s/registries.yaml (on CoulombCore) +mirrors: + "92.205.130.254:32166": + endpoint: + - "http://92.205.130.254:32166" +``` + +k3s picks this up on restart (or SIGHUP). Worker nodes (if any) need the +same file. + +--- + +## Tasks + +### T01 — Makefile: image, push, image-tag targets + +```task +id: KEY-WP-0002-T01 +status: done +priority: high +state_hub_task_id: "749472fc-edb9-4948-9ebc-58d5f38327ee" +``` + +Add `image`, `push`, and `image-tag` targets to `Makefile` with +`IMAGE_REGISTRY`, `IMAGE_REPO`, `IMAGE_TAG` variables defaulting to the +Gitea endpoint and `coulomb/key-cape:latest`. + +Gate: `make image` builds successfully locally; `IMAGE_TAG=dev make image` +produces a differently-tagged image. + +--- + +### T02 — Gitea Actions workflow + +```task +id: KEY-WP-0002-T02 +status: done +priority: high +state_hub_task_id: "8ecf18cc-a3bb-4ede-a09c-fcd0d26d7f9d" +``` + +Create `.gitea/workflows/image.yaml`: +- Trigger: `push` to `main`, `push` tags matching `v*` +- Runner: `act_runner` label (or `ubuntu-latest` if configured) +- Steps: checkout → docker buildx → login → build+push +- Tags via `docker/metadata-action`: `latest` on main, semver on tags + +Secrets required (document in README.md under "CI"): +- `REGISTRY_USER` — Gitea username or machine account +- `REGISTRY_TOKEN` — Gitea personal access token with `write:packages` + +Gate: workflow file is syntactically valid; documented in README. + +--- + +### T03 — k3s insecure registry config on CoulombCore + +```task +id: KEY-WP-0002-T03 +status: done +priority: high +state_hub_task_id: "2dde67f9-944f-418d-a2e9-7367bc556425" +``` + +On CoulombCore, create/update `/etc/rancher/k3s/registries.yaml` to add +the Gitea NodePort as an HTTP mirror. Restart k3s (or send SIGHUP) and +verify `crictl pull 92.205.130.254:32166/coulomb/key-cape:latest` works. + +Gate: image pull from within the cluster succeeds without TLS errors. + +--- + +### T04 — Create Gitea machine account & token + +```task +id: KEY-WP-0002-T04 +status: done +priority: medium +state_hub_task_id: "25775e10-3164-4adb-9c41-835c86fde5f8" +``` + +In Gitea (http://92.205.130.254:32166), create a machine account +`ci-netkingdom` (or reuse an existing service account) with access to +the `netkingdom` organisation. Generate a token with `write:packages` +scope and store it in: +- Gitea Actions secrets on the `key-cape` repo: `REGISTRY_USER`, `REGISTRY_TOKEN` +- The net-kingdom credential store (SOPS-encrypted) under + `credentials/gitea-ci-token.enc.yaml` + +Gate: `docker login 92.205.130.254:32166` succeeds with the token; +secret is in the credential store. + +--- + +### T05 — Smoke test: push and pull a dev image + +```task +id: KEY-WP-0002-T05 +status: done +priority: medium +state_hub_task_id: "0f6ab38f-6d34-41af-9180-f19c687947b5" +``` + +Manually trigger a build-and-push: +```bash +docker login 92.205.130.254:32166 +IMAGE_TAG=dev make push +``` + +Then verify the image is pullable from CoulombCore: +```bash +# on CoulombCore +crictl pull 92.205.130.254:32166/coulomb/key-cape:dev +``` + +Gate: pull succeeds; image is listed in Gitea -> Packages -> coulomb/key-cape. + +--- + +### T06 — Update README with registry & CI docs + +```task +id: KEY-WP-0002-T06 +status: done +priority: low +state_hub_task_id: "946cd34d-94da-4fa9-a781-ed36f6c827a3" +``` + +Add a "Container Image" section to `README.md` documenting: +- Registry URL and image name +- How to pull (`docker pull 92.205.130.254:32166/coulomb/key-cape:latest`) +- How to build and push locally (Makefile targets) +- CI secrets required for the Actions workflow + +Gate: README section present and accurate. + +--- + +## Done Criteria + +- [ ] `make image` and `make push` work locally +- [ ] `.gitea/workflows/image.yaml` present and documented +- [ ] k3s can pull the image from Gitea without TLS errors +- [ ] Machine account token stored in credential store +- [ ] Smoke test: `dev` image pushed and pulled successfully +- [ ] README updated diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md new file mode 100644 index 00000000..9896c67d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md @@ -0,0 +1,200 @@ +--- +id: KEY-WP-0003 +type: workplan +title: "Bootstrap Console OIDC Login and MFA Verification" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-05-24" +updated: "2026-05-24" +state_hub_workstream_id: "02990009-a2b3-44f6-a579-487fbacae41a" +--- + +# KEY-WP-0003 - Bootstrap Console OIDC Login and MFA Verification + +## Problem + +The NetKingdom security bootstrap console now acts as a local OIDC client +callback so the operator can verify the dedicated platform-root login before +approving custody mode. The current live KeyCape deployment rejects that flow +with: + +```json +{ + "error": "invalid_profile_usage", + "description": "redirect_uri does not match any registered URI", + "feature": "redirect_uri" +} +``` + +That error is correct profile enforcement: KeyCape only accepts exact +registered redirect URIs. The live `demo-app` registration has not yet been +updated to allow the local bootstrap console callback: + +- `http://127.0.0.1:8876/oidc/callback` +- `http://localhost:8876/oidc/callback` + +After that is fixed, there is a second usability/security gap. KeyCape checks +privacyIDEA MFA after the Authelia callback, but the browser flow currently +expects an `mfa_token` query parameter instead of presenting a proper OTP +challenge page to the human operator. + +## Goal + +Make the bootstrap console's "Start demo OIDC login" button a real +end-to-end verification path for the current lightweight IAM stack: + +1. KeyCape accepts the bootstrap console callback URI by exact registration. +2. The browser leaves KeyCape for the public Authelia login URL. +3. After password login, KeyCape presents a minimal MFA challenge when + privacyIDEA requires one. +4. KeyCape issues an OIDC authorization code to the bootstrap console callback. +5. The console can exchange the code and let the operator mark + `OIDC login verified` without exposing tokens or secrets. + +This keeps KeyCape's security posture intact: no wildcard redirect URIs, no +dynamic client registration, no token display, and no storage of OTP material. + +## Design Notes + +- Prefer a dedicated public client named `netkingdom-bootstrap-console` for + long-lived clarity. Reusing `demo-app` is acceptable for the immediate + unblock only if the deployment/runbook clearly labels it as a bootstrap test + client. +- The bootstrap callback is local-only and operator-attended. It must be an + exact URI in config, not a wildcard or dynamic registration exception. +- Browser-facing Authelia redirects must use the public Authelia base URL + (`https://auth.coulomb.social`) so the human login page opens correctly. +- KeyCape may still need an internal service URL for back-channel token + exchange. If so, split the current single Authelia URL into browser-facing + authorize URL and internal token URL instead of making the browser use an + in-cluster hostname. +- The MFA prompt should collect only a one-time code, post it back to KeyCape, + validate with privacyIDEA, and then continue the normal OIDC code flow. +- This work unblocks the NetKingdom custody gate in + `NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap`. + +## Implementation Notes + +**2026-05-24:** Implemented in source: + +- added `netkingdom-bootstrap-console` as a public OIDC client in the sample + KeyCape config, while keeping the local callback registered on `demo-app` + for compatibility, +- split Authelia browser redirects from server-side token exchange via + `browserBaseURL` and `tokenBaseURL`, +- added a browser MFA challenge page at `POST /authorize/callback` that + validates the one-time code with privacyIDEA before issuing the downstream + OIDC authorization code, +- updated NetKingdom's `keycape-config` generation template and bootstrap + console to use the dedicated client, +- added regression tests for callback registration, split Authelia URLs, MFA + challenge rendering, valid OTP continuation, and invalid OTP failure. + +Live use still requires deployment: build/publish the updated KeyCape image, +refresh the live `keycape-config` Secret through the custodian age-key unlock +ceremony, and restart the KeyCape deployment. + +--- + +## T01 - Register the bootstrap console callback client + +```task +id: KEY-WP-0003-T01 +status: done +priority: high +state_hub_task_id: "b396c99f-d711-475a-9cba-4f03a1db561d" +``` + +Add a KeyCape client registration for the bootstrap console. Either create a +dedicated `netkingdom-bootstrap-console` public client or update `demo-app` +temporarily with these exact redirect URIs: + +- `http://127.0.0.1:8876/oidc/callback` +- `http://localhost:8876/oidc/callback` + +Update the sample config, tests, and deployment/runbook references so the +registered client is reproducible and not just a live-cluster patch. + +Gate: an authorize request using the local callback no longer returns +`invalid_profile_usage` for `redirect_uri`. + +## T02 - Separate browser-facing and internal Authelia URLs if needed + +```task +id: KEY-WP-0003-T02 +status: done +priority: high +state_hub_task_id: "46172e6d-3e11-493c-b223-79c2fc321aec" +``` + +Confirm whether the current `authelia.baseURL` is safe to use for both browser +redirects and server-side token exchange. If not, add explicit configuration +for the browser authorize base URL and internal token/userinfo base URL. + +Gate: the first browser redirect leaves `https://kc.coulomb.social` for +`https://auth.coulomb.social/...`; server-side token exchange still works from +inside the deployment. + +## T03 - Add a browser MFA challenge step + +```task +id: KEY-WP-0003-T03 +status: done +priority: high +state_hub_task_id: "92fca4d0-6215-4ea6-9f80-9178ae183acb" +``` + +When `CheckMFARequired` returns true after the Authelia callback, render a +minimal KeyCape MFA challenge page instead of requiring `mfa_token` in the +callback query string. The page should: + +- show the authenticated username and client display name, +- collect only the OTP code, +- preserve the pending OIDC state server-side, +- validate with privacyIDEA, +- continue to issue the normal authorization code on success, +- fail closed with the existing telemetry on invalid MFA. + +Gate: a user enrolled in privacyIDEA can complete password + OTP in the +browser and is returned to the registered downstream callback. + +## T04 - Add end-to-end profile tests for the bootstrap login path + +```task +id: KEY-WP-0003-T04 +status: done +priority: medium +state_hub_task_id: "079a5929-1864-4461-a64c-746cebca469d" +``` + +Add tests that cover: + +- local bootstrap callback registration, +- rejection of unregistered callbacks remains intact, +- Authelia browser redirect uses the expected public URL, +- MFA-required login presents a challenge instead of immediate failure, +- invalid OTP fails closed, +- valid OTP produces an authorization code bound to the original PKCE session. + +Gate: `make test` passes and the negative redirect URI tests remain green. + +## T05 - Document the live rollout ceremony + +```task +id: KEY-WP-0003-T05 +status: done +priority: medium +state_hub_task_id: "1d67225d-a20b-4e36-9b2e-20836be2f439" +``` + +Document the deployment path for updating live KeyCape config without +regenerating unrelated secrets. The runbook must fit the NetKingdom custodian +age-key model: decrypt or unlock only during an attended ceremony, apply the +updated client registration/config, restart KeyCape, and remove plaintext +secret material afterward. + +Gate: an operator can update the live `keycape-config` Secret and verify the +bootstrap console OIDC login without printing or committing secrets. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md new file mode 100644 index 00000000..a50e9105 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md @@ -0,0 +1,332 @@ +--- +id: KEY-WP-0004 +type: workplan +title: "Binky Hedgehog GmbH as first NetKingdom tenant + qonto runtime workload identity" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-07-23" +updated: "2026-07-29" +state_hub_workstream_id: "817709be-5756-5ef7-9ccd-dcb84b6b9c64" +--- + +# Binky Hedgehog GmbH as first NetKingdom tenant + qonto runtime workload identity + +Binky Hedgehog GmbH ("binky") becomes the **first real tenant** provisioned in +the NetKingdom IAM stack, per the NetKingdom IAM Profile v0.2's tenant claim +model (`net-kingdom/canon/standards/iam-profile_v0.2.md` §"Tenant Claim") — +distinct from `tenant:platform` (control plane) and `tenant:coulomb` (the +ecosystem-development tenant this workstation already operates under). + +This closes **Gap #1** flagged in `qonto-assistant`'s +`workplans/QONTO-WP-0003-mcp-surface.md` closure: no OIDC issuer existed +anywhere in the fleet, so `qonto-assistant`'s MCP surface shipped with an +interim shared-secret bearer token instead of real workload identity. +`key-cape` is "Implementation complete (v0.1)" and already implements the +profile's `client_credentials`/workload-identity grant for service accounts +— it's the component that should close this gap, not `user-engine` +(downstream user/profile data layer, not an identity provider) or +`identity-canon` (terminology research only). + +**Depends on:** `key-cape` v0.1 (done, `KEY-WP-0001`), NetKingdom IAM Profile +v0.2 tenant + service-account claim rules (already ratified canon — no new +profile work needed, this is provisioning against an existing contract), and +the cross-repo runtime direction now tracked in +`railiance-master/workplans/RMASTER-WP-0019-knative-qonto-runtime-on-reef-railiance.md`. + +This workplan no longer targets a permanent direct `qonto-assistant` +deployment shape. Its workload-identity lane is intended for the future +`rapp-qonto` runtime on `rail-knative` hosted by `reef-railiance`, with +current `qonto-assistant` consumption allowed only as a migration bridge until +that package/rail path exists. + +## Task: Confirm tenant identifier and provision the tenant plane + +```task +id: KEY-WP-0004-T01 +status: done +priority: high +state_hub_task_id: "c3bdbdb6-6b3f-51b5-8521-d67f113f1c3d" +``` + +**Decided 2026-07-23 (Bernd):** the IAM profile's `tenant:customer:` / +`tenant:sandbox:` suggested identifiers name a tenant by **capability +role** (customer, vendor, ...) — but tenants already known to hold multiple +roles at once (see `net-kingdom/docs/princedom-isolation-exploration.md`'s +unratified `PLTF`/`IAM`/`VEN`/`CUS` role model; Binky itself is both `CUS` +and, later, `VEN`). Using a role word as the tenant *grouping* would collide +with that separate role dimension. Decision: classify tenants by an +onboarding-risk / entity-shape **grouping**, orthogonal to capability role: + +```text +trial — test/trial/showcase tenants only +friendly — known, easily reached, tolerant of experimentation/instability +single — one-person business entities (freelance consultants) +small — up to 10 employees at time of onboarding (attoo) +medium — up to 100 employees (attoo) +large — up to 1000 employees (attoo) +enterprise — 1001+ employees (attoo) +consumer — private individuals +family — a legal family +community — a non-legal group of people +association — a legal association of people +agentic — financially enabled AI entities +``` + +`tenant::` replaces `tenant:customer:` / +`tenant:sandbox:` as the identifier shape. **Binky maps to `friendly`** +— known, reachable, tolerant of early instability — giving +**`tenant:friendly:binky`** (slug matches the OpenBao path already in +production, `tenants/binky/qonto-api`). + +**Open question, not yet resolved by this decision:** this grouping list +classifies tenants as external-shaped entities: it has no obvious slot for +`tenant:coulomb` itself (the platform's own internal/reference tenant, +ADR-0006) — none of `trial`/`friendly`/`single`/.../`agentic` fit "the +platform building itself." Confirm with Bernd whether `tenant:coulomb` stays +an ungrouped special case (alongside `tenant:platform`) or gets folded into +this scheme somehow, before treating the grouping list as complete. + +**Also open:** this is a fleet-wide vocabulary decision — the ratified +`net-kingdom/canon/standards/iam-profile_v0.2.md` "Tenant Claim" section +still documents the old `customer`/`sandbox` suggested identifiers, and +NetKingdom owns that canon per ADR-0011. Recording this grouping list only +in this key-cape workplan lets it drift from canon. Recommend a follow-up: +either a decision record or an ADR in `net-kingdom` adopting this grouping +list as the tenant-identifier standard, superseding the old suggested +identifiers. Not drafted here — flagging so it isn't lost. + +Provision whatever tenant/org-unit boundary key-cape's current backend +(LLDAP/Authelia) needs so tokens issued for subjects under this tenant +correctly carry `tenant:friendly:binky` and nothing implies platform-root +authority for it (profile rule: tenant administration for a tenant must +never imply platform-root authority). + +Done when: `tenant:coulomb`'s place in the grouping scheme is resolved; the +`net-kingdom` canon-update question above is answered (drafted or explicitly +deferred); a test-subject token carries the `tenant:friendly:binky` claim. + +2026-07-27: IAM Profile v0.2 now preserves `tenant:platform` and +`tenant:coulomb` as reserved ungrouped tenants and ratifies the external +grouping vocabulary. The live service-token proof carries +`tenant:friendly:binky` without platform authority. + +## Task: Provision bernd.worsch@binky-hedgehog.com as tenant-admin + +```task +id: KEY-WP-0004-T02 +status: done +priority: high +state_hub_task_id: "202c419e-838f-5c2f-8a52-9b1c36a6d6b9" +``` + +Create the user in key-cape's current backend, enroll MFA per profile +requirements (`assurance` claim, `net-kingdom/canon/standards/iam-profile_v0.2.md`), +and assign a tenant-admin role/group scoped to `tenant:friendly:binky` only. + +2026-07-27 direction update: do not complete this through a one-shot operator +script. `USER-WP-0020` and `NK-WP-0023` now own a reusable self-service and +administration portal plus NetKingdom provisioning adapters. This task is +their first production acceptance case. + +The `binky-hedgehog.com` mailbox must exist and be reachable for account +verification, but mailbox availability is now an input to the reusable +registration flow rather than a manual provisioning procedure. + +Done when: `bernd.worsch@binky-hedgehog.com` completes OIDC/PKCE + MFA login +and receives a token carrying `tenant:friendly:binky` and a tenant-admin role +claim; the same token does **not** grant access to `tenant:platform` or any +other tenant. + +2026-07-28: user-engine's deployed portal API created the tenant-admin +membership and provisioned/linked the LLDAP identity through the reusable +NetKingdom adapter. The identity is intentionally in +`password_setup_required`; LLDAP has no SMTP configuration for its native +reset delivery. First-password handoff, MFA enrollment, and final human-token +claim/denial evidence remain. No operator-set password or raw credential was +used as a shortcut. + +2026-07-28 update: the reusable password handoff is now live at the canonical +KeyCape host and is issued/renewed through the user-engine administration UI. +Links are opaque, expire after 15 minutes, are single use, and older links are +revoked on renewal. A disposable-user live test proved password registration, +directory login, replay denial, and cleanup. This task now awaits only the +Binky user's own password choice, MFA enrollment, and final scoped-token +acceptance. + +2026-07-29: the first portal attempt correctly denied cross-tenant access +because KeyCape had not projected the established `net-kingdom-admins` group +to the new platform-operator role. Commit `90a2078` adds and tests the explicit +`tenant:platform` / `platform-operator` compatibility mapping and is deployed +Ready. The operator must start a fresh OIDC session so the corrected claims +are minted. + +2026-07-29 human acceptance: the Binky administrator completed the reusable +password setup, enrolled a privacyIDEA TOTP factor, and successfully signed in +through KeyCape with password plus OTP. Enrollment revealed that privacyIDEA's +default QR label exposed only the token serial. The live +`coulomb-friendly-token-labels` enrollment policy now emits issuer `Coulomb` +and label `{user}@{realm}` for future tokens; existing authenticator entries +must be renamed locally because wallets do not accept remote label updates. +The live token then carried `tenant:friendly:binky`, roles `user` and +`tenant-admin`, only the Binky users/admins groups, and AAL2 assurance with +`pwd` + `otp` and `mfa: true`. The same session was denied access to both +`tenant:platform` and `tenant:coulomb` administration. Acceptance complete. + +## Task: Register a workload-identity OIDC client for the qonto runtime + +```task +id: KEY-WP-0004-T03 +status: done +priority: high +state_hub_task_id: "0d3ccd35-bad5-5c30-95a3-61fce584425d" +``` + +Register an OIDC client per the profile's service-account/workload-identity +flow (`client_credentials` or the documented workload-token-exchange +equivalent — §"Service Account Flow"), scoped to `tenant:friendly:binky`, +with the minimum role/scope the qonto runtime surface actually needs. The +intended steady-state consumer is the future `rapp-qonto` runtime on +`rail-knative`; current `qonto-assistant` callers may use the same lane only +as a documented migration bridge until that package split exists. No +interactive login for this client — it's for agent-harness sessions and other +automated callers, not humans. + +The resulting `client_secret` must never be pasted into chat, Git, this +workplan, or a State Hub message or progress event — hand-off happens only +through T04's secrets-engine/OpenBao lane. + +Done when: a `client_credentials` token exchange against key-cape succeeds +for this client and produces a short-lived service token carrying +`tenant:friendly:binky`; the `client_id`, granted scopes, and intended future +binding to `rapp-qonto` are documented here; no secret material appears +anywhere in this repo's history. + +2026-07-26: Added `docs/qonto-runtime-identity-contract.md` with stable +principal/client names, least-privilege claims, non-secret evidence, and the +automation/custody boundary. Actual client issuance remains open. + +2026-07-27: `KEY-WP-0006` delivered `rapp-qonto-client`. Live exchange +produced only the documented tenant, role, and `qonto:read` scope; wrong +secret and excessive scope were denied. + +## Task: Route client_secret custody through secrets-engine + OpenBao + +```task +id: KEY-WP-0004-T04 +status: done +priority: high +state_hub_task_id: "9cb09dbf-1fd0-55c5-9c45-b3b7296ecd91" +``` + +Coordinate with `secrets-engine` to create a scoped OpenBao lane for this +`client_secret`, mirroring the existing `tenants/binky/qonto-api` pattern +already in production use for the Qonto bank credential itself. The steady +target is a dedicated future runtime role for `rapp-qonto`; if a temporary +bridge via the current `qonto-assistant` runtime role is needed before the +package split lands, it must be recorded as temporary rather than treated as +the final contract. + +Done when: the intended qonto runtime role can fetch the `client_secret` via +`secrets-engine`/OpenBao; any temporary `qonto-assistant` bridge is explicitly +marked transitional; Bernd never handles the raw value at any point in this +flow. + +2026-07-26: `warden plan` classified first-time provisioning as one explicit +founder authority act through `warden desk`. No secret was requested or read. +All repeatable verification and revocation work is designated for sanctioned +agent execution and later functional automation. + +2026-07-27: Custody is established at +`platform/workloads/rapp-qonto/keycape-client`, with runtime injection through +`sso/keycape-rapp-qonto-client`. No raw value entered evidence or human +handling. + +## Task: Register the credential lane with ops-warden + +```task +id: KEY-WP-0004-T05 +status: done +priority: high +state_hub_task_id: "39f1f803-732e-5905-97d9-c7493c66c8ee" +``` + +This is the task that directly minimizes Bernd's ongoing engagement: add an +entry to ops-warden's routing catalog (`registry/routing/catalog.yaml`) +pointing at this new OIDC client / login lane, following the pattern already +documented in `ops-warden/SCOPE.md`'s "Issue vs route" table ("Login / OIDC / +MFA → key-cape / Keycloak → Assist — route; proxy `login` lane when +`exec_capable`"). ops-warden does not mint tokens or take custody here — it +routes/assists, same as every other credential lane it fronts. The lane should +describe the future `rapp-qonto` runtime path on `rail-knative`, not only the +current source repo. + +Once the OpenBao lane from T04 exists, run ops-warden's workload security +posture conformance checker +(`ops-warden/scripts/check_secret_posture_conformance.py`) against the qonto +runtime workload that will later become `rapp-qonto`; until the split lands, +`qonto-assistant` may be used as the migration subject if that temporary state +is recorded. Confirm it meets the expected maturity tier (dev/test/prod +posture, M0–M3 workload maturity) before this goes live with real +credentials, and record the result (pass or documented exception). + +Done when: `warden route show` (or equivalent) resolves this lane correctly; +posture conformance check passes or exceptions are explicitly recorded; from +this point on, discovering and using this credential lane needs `warden +access`, not direct key-cape/OpenBao admin knowledge. + +2026-07-26: Routing requirements and required conformance evidence are now +documented. Catalog publication waits for the real client and OpenBao role so +the route does not advertise an unusable lane. + +2026-07-27: Published route `rapp-qonto-keycape-client`; route resolution and +the metadata-only M3/prod posture check both pass. + +## Task: automation@binky-hedgehog.com service identity (deferred) + +```task +id: KEY-WP-0004-T06 +status: cancel +priority: low +state_hub_task_id: "fbee11fc-7a60-5dd7-8522-f80ddb4aea55" +``` + +Forward-looking per Bernd's framing ("I guess it will later be helpful") — +**do not provision until a concrete automation workflow actually needs its +own identity** distinct from the qonto runtime workload client from T03. +Dormant service-account credentials are themselves a posture risk (unused +attack surface, nothing to rotate against), so this stays parked rather than +front-loaded. + +When a real consuming workflow exists: same pattern as T02 but scoped to an +automation-appropriate role (not tenant-admin), likely paired with its own +`client_credentials` registration for whichever agent/service authenticates +as it, following T03/T04/T05's pattern rather than inventing a new one. + +Done when: explicitly triggered by a named consuming workflow — until then +this task stays `todo` and does not block closing the rest of this workplan. + +## Task: Closure review + +```task +id: KEY-WP-0004-T07 +status: done +priority: low +state_hub_task_id: "77b0556f-62fd-5046-9ba1-718246151a6f" +``` + +Mark workplan finished when T01–T05 are done (T06 may legitimately remain +`todo`/parked by design — see its own task note). Confirm end to end: a +human token (bernd.worsch@binky-hedgehog.com, tenant-admin) and a service +token (future qonto runtime client, with any temporary `qonto-assistant` +bridge explicitly recorded) both resolve to `tenant:friendly:binky` and +nothing else; the credential lane is discoverable via `warden access` without +Bernd touching key-cape/OpenBao admin surfaces directly. The runtime follow-up +then continues in `QONTO-WP-0004` and `RMASTER-WP-0019`. Run +`statehub fix-consistency`. + +2026-07-29 closure: T01–T05 are complete, T06 is intentionally cancelled, +the human Binky flow passed AAL2 and tenant-isolation acceptance, and the qonto +workload client passed its positive-scope and negative credential/scope checks. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md new file mode 100644 index 00000000..648d2405 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md @@ -0,0 +1,245 @@ +--- +id: KEY-WP-0005 +type: workplan +title: "IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-07-23" +updated: "2026-07-23" +state_hub_workstream_id: "711a8303-69aa-5db6-9043-92530c6dc81b" +--- + +# IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles + +Discovered while planning `tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md`: +`key-cape`'s token issuance (`src/internal/server/oidc/token.go`) does not +currently emit `tenant`, `principal_type`, `groups`, `roles`, or +`assurance` — none of `net-kingdom/canon/standards/iam-profile_v0.3.md`'s +Core Claims are implemented yet, only `preferred_username`/`email`/`groups` +(LDAP-sourced, not the profile's normalized `groups` claim) and OIDC's own +base claims (`nonce`, etc.). + +This is bigger than "add the `tenant_roles` claim on top of existing tenant +support" — `key-cape` isn't yet profile-conformant at all on the tenant/ +principal/assurance axis. `tenant_roles` (the reason this workplan exists) +is the last, optional claim in that same set (v0.3's addition on top of +v0.2's baseline) — implementing it in isolation without the claims it +depends on for meaning (`tenant`, `principal_type`) would be incoherent. + +**This workplan is registered, not implemented, by the same session that +found the gap.** Token issuance is security-sensitive code in a Go codebase +with its own adapter conventions (`internal/adapters/{lldap,privacyidea,authelia}`) +that deserves dedicated context before changing, not a rushed edit from a +first read of `token.go`. Scoped and sequenced here so the work isn't lost. + +**Depends on:** `net-kingdom/canon/standards/iam-profile_v0.3.md` (ratified). +**Related:** `tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md` +(the consumer waiting on `tenant_roles`); `key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md` +(Binky's onboarding, which needs `tenant` claim support to produce a +correct `tenant:friendly:binky` token at all). + +## Task: Emit `tenant`, `principal_type`, `groups`, `roles`, `assurance` + +```task +id: KEY-WP-0005-T01 +status: done +priority: high +state_hub_task_id: "32d5fbdc-cd2b-5bc8-9d2b-1a410b55e806" +``` + +Bring `token.go`'s claim assembly up to `iam-profile_v0.3.md`'s Core Claims +table baseline (the v0.2-originated set): + +- `tenant`: resolve from whatever user/service/agent-to-tenant mapping + `key-cape`'s LLDAP/Authelia backend can provide today; if no such mapping + exists yet, that's itself a sub-task — this claim cannot be stamped from + nothing. +- `principal_type`: `human` for the existing Authorization Code + PKCE flow; + `service` for client-credentials flows (check whether `key-cape` already + issues client-credentials tokens at all — `grant_types_supported` in the + discovery contract claims it does, but that needs verifying against real + code, the same way this workplan verified `tenant` claim absence). +- `groups`, `roles`: normalize from LDAP group/role data into the profile's + flat string-array shape. +- `assurance`: `{level, methods, mfa, source, at}` per the profile — + `privacyIDEA`-backed MFA evidence maps to `aal2`; single-factor maps to + `aal1`. + +Done when: `tools/iam-profile-conformance/` (already exists) passes against +these claims for both human and service token issuance paths. + +**Done 2026-07-24, human flow only — scope split made explicit, not +silently narrowed:** verified first (as flagged): `grant_types_supported` +advertises `client_credentials` in `discovery.go`, but `token.go`'s +`ServeHTTP` only ever accepted `grant_type == "authorization_code"` — there +is no service-token issuance path at all, so "verifying against service +token issuance" isn't possible until that path exists. Building a +`client_credentials` grant handler from scratch is a distinct, materially +larger piece of work than extending the existing human flow (new endpoint +logic, new client-authentication semantics, no existing tests to extend) +— **not attempted in this pass**, left open below rather than declared done. + +What shipped for the human Authorization Code + PKCE flow: + +- `domain.User.Tenant` (new field, `omitempty`, empty-safe for existing + YAML configs) + `token.go`'s `effectiveTenant()`: falls back to + `tenant:coulomb` (this workstation's actual current tenant, ADR-0006) + when unset — never emits an empty `tenant` claim, never silently assigns + a user to a tenant they weren't given. +- `principal_type: "human"`, unconditional. +- `groups`/`roles`: promoted from scope-gated (only `groups`, only with the + `groups` scope) to unconditional core claims, always present as `[]` + when empty, never `null` (`nonNilStrings()`). One pre-existing test + asserted the old scope-gated behavior for `groups` + (`TestTokenHandler_ScopeFiltering_ProfileScope`) — updated to assert the + new, intentional behavior, not silently left failing or reverted. +- `assurance`: built from whether MFA was *actually verified in this + session* (`PKCESession.MFAVerified`, new field, threaded through + `completeAuthorization`'s two call sites in `authorize.go`), not from + static enrollment state — `aal2` only when MFA was required and passed + this time, `aal1` otherwise. + +Verified for real: `go build ./...` and `go vet ./...` clean; `go test +./...` green across the whole repo (one pre-existing test updated to match +the new intentional behavior, described above, not silently broken). Two +new `authorize_test.go` cases directly assert `PKCESession.MFAVerified` is +set correctly on both the MFA-required and MFA-not-required paths. +`tests/profile/profile_test.go`'s `TestCompleteTokenFlow` — the repo's own +full HTTP integration test (real server, real PKCE flow, real JWT decode) +— extended with real assertions for all five new/changed claims, not just +presence checks: `tenant == "tenant:coulomb"` (the fallback path, since +the test's `testuser` has no explicit tenant), `principal_type == "human"`, +`groups == ["developers"]`, `roles == []`, and +`assurance == {level: "aal1", mfa: false, source: "key-cape", ...}` (this +test's `mockMFA` never requires MFA, so `aal1` is the correct expected +value, not `aal2`). + +Not run: `net-kingdom/tools/iam-profile-conformance`'s Python conformance +tool against a live minted token — it needs a reachable issuer, which would +mean standing up the full `docker-compose.dev.yml` stack +(Authelia+LLDAP+privacyIDEA) beyond what's practical in this pass. The +equivalent claim-shape and value checks it would perform are covered by +`TestCompleteTokenFlow`'s real HTTP round trip instead — not a like-for-like +substitute, but real coverage, not an assumption. + +**Explicitly still open** (not this task, not silently dropped): +`client_credentials` grant handling — service-token issuance for `key-cape` +callers with no human in the loop (`tenant-engine`'s own eventual key-cape +integration, and any other service caller). Needs its own scoped follow-up: +a new `principal_type: "service"` code path in `token.go` (or a sibling +handler), client secret/authentication semantics, and its own test suite — +this workplan's `T02` (`tenant_roles`) doesn't strictly require it, since +`tenant_roles` attaches to whatever principal_type a token already carries, +human included. + +## Task: Emit `tenant_roles` (optional, cached) + +```task +id: KEY-WP-0005-T02 +status: done +priority: high +state_hub_task_id: "c3ee6021-b28d-50a5-be60-ecb5631f56e9" +``` + +At token-issuance time, call `tenant-engine`'s cache-read endpoint +(`GET /tenants/{tenant_id}/roles`) using the `tenant` claim resolved in T01, +and stamp the result as the optional `tenant_roles` claim +(`iam-profile_v0.3.md`, "Tenant Roles" section). Follows the same adapter +pattern as `internal/adapters/{lldap,privacyidea,authelia}` — a new +`internal/adapters/tenantengine` package, not inline HTTP calls in +`token.go`. + +Fail open on this claim specifically, not closed: `tenant_roles` is +documented as a cache callers must not trust for privileged decisions +anyway (`flex-auth` re-validates live). If `tenant-engine` is unreachable at +issuance time, omit the claim rather than fail the entire token issuance — +losing a performance optimization is acceptable; blocking login because a +cache source is down is not. This is the one place in the whole `tenant_roles` +design where fail-open is correct, precisely because the live-lookup path +exists as the real gate. + +Done when: token issuance still succeeds with `tenant_roles` omitted when +`tenant-engine` is unreachable (test simulates the outage); present and +correct when reachable, for a tenant with known role grants. + +**Done 2026-07-24:** New `internal/adapters/tenantengine` package +(`Client.Roles(ctx, tenantID) ([]string, bool)`), mirroring +`internal/adapters/{lldap,privacyidea,authelia}`'s shape exactly. Fails +open by construction — unreachable, non-200, or malformed body all return +`(nil, false)`, never an error the caller has to specially handle; a `nil +*Client` also fails open safely rather than panicking, so `TokenHandler` +doesn't need a separate "is this configured" branch. Wired into +`TokenHandler.TenantEngine` (nil by default — existing tests and call +sites are unaffected); `token.go` stamps `tenant_roles` only when `ok`. + +7 adapter-level tests (success, non-200, malformed body, connection +failure, timeout, nil client, empty base URL). 3 new `token_test.go` cases +prove the actual required behavior end-to-end through `TokenHandler`, not +just at the adapter layer: present and correct when reachable; **token +issuance still returns 200 with every other required core claim intact** +when unreachable, `tenant_roles` simply absent (the literal done-criteria); +absent when `TenantEngine` isn't configured at all. `go build`/`vet` +clean, `go test ./...` green repo-wide. + +**A real bug found and fixed along the way, not worked around:** the first +live cross-process check (real `flex-auth`, real `tenant-engine`, this +adapter) returned `tenant_not_found` for a tenant that genuinely existed. +`tenant-engine`'s `GET /tenants/{tenant_id}/roles` was keyed by its +internal `tenant_id`, but `key-cape` (like any external caller) only ever +has the tenant's profile *identifier* (the `tenant` claim value) — it has +no way to know tenant-engine's internal id. Fixed at the source: +`tenant-engine/workplans/ADHOC-2026-07-24.md` (identifier-or-id resolution +added to `InMemoryTenantStore`), re-verified with the exact same live +three-process chain afterward — `roles=[IAM] ok=true`, resolved by +identifier. This is exactly why T02 insisted on a live-server-reachable +test rather than only an `httptest` fake: the fake would have used +whatever id shape the test author happened to pick and never caught this. + +## Task: Closure review + +```task +id: KEY-WP-0005-T03 +status: done +priority: low +state_hub_task_id: "c88a3457-2964-5c44-8220-76f82df3a753" +``` + +Confirm T01–T02 done; conformance suite green for both tasks together. +Note in closure: this unblocks `KEY-WP-0004`'s Binky onboarding actually +producing conformant tokens, and unblocks `flex-auth`'s side of the +`tenant_roles` picture receiving a real claim to reason about (though +`flex-auth`'s live-lookup path doesn't depend on this claim existing — only +the cache-read/performance path does). Run `statehub fix-consistency`. + +**Closed 2026-07-24.** T01–T02 done. `go build ./...`, `go vet ./...` clean; +`go test ./...` green across the whole repo. Python +`net-kingdom/tools/iam-profile-conformance` not run against a live instance +(needs the full Authelia+LLDAP+privacyIDEA stack, impractical in this +pass) — real coverage instead came from `tests/profile`'s own full HTTP +integration test (`TestCompleteTokenFlow`) with genuine claim-value +assertions, and from three separate live cross-process checks against a +real `tenant-engine` (one of which found and fixed a real bug in +`tenant-engine` itself, `ADHOC-2026-07-24`). + +**What this closes:** `key-cape`'s human Authorization Code + PKCE flow now +emits every IAM Profile v0.3 core claim (`tenant`, `principal_type`, +`groups`, `roles`, `assurance`) plus the optional `tenant_roles` cache, +correctly sourced from a real `assurance` signal (MFA actually verified +this session, not static enrollment) and a real `tenant-engine` call +(fail-open, never blocking login). `KEY-WP-0004`'s Binky onboarding can now +produce conformant tokens for human logins. + +**What stays explicitly open, not silently dropped:** +- `client_credentials` / service-token issuance — no such flow exists in + `token.go` at all yet (confirmed in T01); needed before any *service* + caller (not a human) can get a token with `principal_type: "service"` and + its own `tenant`/`tenant_roles`. A materially larger, separate piece of + work than either task in this workplan. +- `flex-auth`'s side of `tenant_roles` (`FLEX-WP-0008`, already closed + separately) doesn't depend on this claim existing at all — its + live-lookup path calls `tenant-engine` directly, so this workplan doesn't + block it, but also doesn't complete the full `tenant_roles` picture on + its own until service tokens exist too. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md new file mode 100644 index 00000000..632c4c2c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md @@ -0,0 +1,116 @@ +--- +id: KEY-WP-0006 +type: workplan +title: "Client credentials and service-token issuance" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-07-27" +updated: "2026-07-27" +state_hub_workstream_id: "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d" +--- + +# KEY-WP-0006 - Client credentials and service tokens + +Implement the missing service-principal half of the IAM Profile so +`KEY-WP-0004` can issue a least-privilege identity to `rapp-qonto`. + +## T01 - Model confidential service clients + +```task +id: KEY-WP-0006-T01 +status: done +priority: high +state_hub_task_id: "993913a7-f951-50ee-9b10-d5b9e67b523c" +``` + +Extend static client registration with an explicit service subject, tenant, +roles, and an environment-backed secret reference. Refuse startup when a +`client_credentials` client is public, lacks its identity fields, or cannot +resolve its secret. Never serialize or log the resolved secret. + +2026-07-27: Added explicit service subject, tenant, roles, and in-memory-only +resolved secret fields. Configuration validation rejects public or incomplete +service clients, and startup resolves only `env:` secret references while +reporting the variable name—not its value—on failure. + +## T02 - Implement secure client_credentials exchange + +```task +id: KEY-WP-0006-T02 +status: done +priority: high +state_hub_task_id: "801a6d55-2f91-51b1-88e4-cc7f0867238d" +``` + +Accept confidential client authentication through HTTP Basic, compare secrets +in constant time, restrict requested scopes to the registered allowlist, and +issue a short-lived access token with `principal_type=service`, +tenant-specific subject/roles, empty groups, `aal1` client-secret assurance, +and optional cached `tenant_roles`. Do not issue an ID token for this +non-human grant. + +2026-07-27: Implemented HTTP Basic confidential-client authentication with +fixed-length SHA-256 constant-time comparison, allowlisted scopes, short-lived +service claims, optional tenant-role cache, token-issued telemetry, and no ID +token. + +## T03 - Prove positive and negative conformance + +```task +id: KEY-WP-0006-T03 +status: done +priority: high +state_hub_task_id: "e5c2a8b1-0879-57fa-a197-250a34c75bdb" +``` + +Cover valid exchange, unknown client, public client, missing/wrong secret, +unsupported grant, excess scope, tenant isolation, telemetry redaction, and +discovery metadata. Run the full Go build, vet, and test suite. + +2026-07-27: Added positive service-claim and negative wrong-secret/excess-scope +tests, updated discovery to advertise only implemented grants/auth methods, +and verified `go test ./...`, `go vet ./...`, and `go build ./...` across the +module. + +## T04 - Provision and verify rapp-qonto + +```task +id: KEY-WP-0006-T04 +status: done +priority: high +state_hub_task_id: "31af6b8c-96ff-5060-87db-3803b9a9dd12" +``` + +Generate the client secret without disclosure, store it through the approved +OpenBao lane, register the static `rapp-qonto` client, deploy KeyCape, and +verify a real exchange yields only the documented Binky service claims. +Publish non-secret evidence to `KEY-WP-0004-T03` through T05. + +2026-07-27: Provisioned the confidential client secret at +`platform/workloads/rapp-qonto/keycape-client`, injected it into +`sso/keycape-rapp-qonto-client`, registered `rapp-qonto-client`, and deployed +image `main-e877d27-2` on railiance01. A real exchange produced only +`sub=rapp-qonto`, `tenant=tenant:friendly:binky`, `principal_type=service`, +`roles=[qonto-reader]`, empty groups, and `scope=qonto:read`; excessive scope +and a wrong secret were denied. Public DNS still resolves +`kc.coulomb.social` to the older CoulombCore endpoint, so the deployment proof +used TLS-preserving direct resolution to railiance01 (`92.205.62.239`). + +## T05 - Closure review + +```task +id: KEY-WP-0006-T05 +status: done +priority: low +state_hub_task_id: "8c72d7d9-4086-5e64-b939-a12bff076124" +``` + +Close after T01-T04 pass and the repeatable verification path is documented. + +2026-07-27: Closure review passed. Build, vet, unit/profile tests, live +positive claims, negative authentication/scope checks, and discovery metadata +all passed. The stale public DNS target is handed off as routing work rather +than weakening the service-token contract. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md new file mode 100644 index 00000000..f1b04e1a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md @@ -0,0 +1,81 @@ +--- +id: KEY-WP-0007 +type: workplan +title: "User-engine portal OIDC relying-party integration" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-07-27" +updated: "2026-07-29" +depends_on: + - KEY-WP-0005 + - USER-WP-0020 +state_hub_workstream_id: "24db4178-705c-5b09-b7db-ee7a345d71a0" +--- + +# KEY-WP-0007 - User-engine portal OIDC relying-party integration + +Register and prove the reusable user-engine portal as a strict KeyCape public +client. This is the authentication edge required by `NK-WP-0023-T03`; it does +not move user-domain or authorization ownership into KeyCape. + +## Register the static portal client + +```task +id: KEY-WP-0007-T01 +status: done +priority: high +state_hub_task_id: "7d722cae-bf6d-52e0-80f4-cc51e9fb42f0" +``` + +Register `user-engine-portal` for authorization code with mandatory S256 PKCE, +exact callback URIs and only `openid profile email groups`. No wildcard, +implicit flow, client secret, or dynamic registration is allowed. + +## Deploy and verify the live client + +```task +id: KEY-WP-0007-T02 +status: done +priority: high +state_hub_task_id: "0b222062-4352-5ae6-9e85-95787af08409" +``` + +Update the railiance01 KeyCape configuration without exposing its signing key +or backend credentials. Verify discovery, authorize redirect validation, +unregistered callback denial and successful token exchange through the portal. + +## Prove claims and MFA isolation + +```task +id: KEY-WP-0007-T03 +status: done +priority: high +state_hub_task_id: "859e7322-71dc-58cc-9f44-6f869c2d0b59" +``` + +Prove issuer, audience, tenant, groups, roles and assurance claims are verified +by the portal and that tenant administration does not imply platform-root. +Complete the Binky user/MFA acceptance through the reusable browser path. + +2026-07-27: The live client accepts only the exact portal callback and rejects +an unregistered callback with `invalid_profile_usage`. The portal begins an +S256 PKCE flow and hands authentication to Authelia. LLDAP tenant envelope +mapping now recognizes unambiguous `tenant:::users|admins` groups; +ambiguous multi-tenant directory envelopes fail closed to no explicit tenant. + +2026-07-28: suspension now fails closed at both token exchange and +`/userinfo`; the deployed `key-cape:909bb32` image refuses subjects carrying +the `netkingdom-suspended` directory group. The portal admin surface uses this +through the provider-neutral lifecycle API. The human Binky MFA/claims +acceptance remains the final part of this task. + +2026-07-29 complete: the reusable browser path issued the Binky password +handoff, completed privacyIDEA TOTP enrollment, and returned through the +portal authorization-code flow. `/api/v1/me` verified the canonical issuer, +`user-engine-portal` audience, `tenant:friendly:binky`, Binky-only groups, +roles `user` + `tenant-admin`, and AAL2 assurance with `pwd` + `otp` and +`mfa: true`. The same session was denied administration of +`tenant:platform` and `tenant:coulomb`. All workplan tasks are complete. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md new file mode 100644 index 00000000..438ab548 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md @@ -0,0 +1,118 @@ +--- +id: KEY-WP-0008 +type: workplan +title: "Registration handoff and client-aware MFA policy" +domain: infotech +repo: key-cape +status: finished +owner: grok +topic_slug: netkingdom +created: "2026-08-09" +updated: "2026-08-16" +depends_on: + - NK-WP-0025 +state_hub_workstream_id: "23eb67d4-cf10-57a1-92f6-90146a294bda" +--- + +# KEY-WP-0008 - registration handoff and client-aware MFA + +Let a registered OIDC client offer NetKingdom signup and request step-up +without making KeyCape an account store or weakening high-assurance clients. + +## T01 - Add a safe registration handoff + +```task +id: KEY-WP-0008-T01 +status: done +priority: high +state_hub_task_id: "e5fb4d73-1e19-565c-80d5-e0eee274ce63" +``` + +Define an allow-listed registration URL for eligible clients and preserve +client ID, redirect URI, PKCE intent, tenant hint, and return context in a +signed, expiring state envelope. Registration completion must restart the +normal authorization flow and must not mint a token directly. + +Done when unknown users can choose signup from an eligible authorization flow +without open redirect, client substitution, or state replay. + +Implemented `prompt=create` and `/authorize/register` against the client's +static `registrationUrl`, plus HMAC-signed `kc_handoff` envelopes consumed +once at `/authorize/return`. Return restarts `/authorize` and never mints a +code. Ineligible clients get no signup link. Live registration entry remains +user-engine-owned per NK-WP-0025; KeyCape only issues the return envelope. + +## T02 - Replace global MFA with client-aware minimum assurance + +```task +id: KEY-WP-0008-T02 +status: done +priority: high +state_hub_task_id: "1ba50454-b6f5-578e-a526-da74b8082a89" +``` + +Replace the single require-for-all switch with policy that combines client +minimum assurance, requested ACR/step-up, tenant policy, protected action, and +current session assurance. Preserve mandatory MFA for platform/admin clients. +Allow coulomb-social ordinary login at password assurance when no stronger +rule applies. + +Done when one low-assurance client cannot suppress MFA for another client or +reuse an under-assured session for a high-assurance request. + +Implemented with nullable per-client `mfaRequired`: an explicit client value +overrides the provider default only for that client. Absent values preserve +the existing provider-driven policy. + +2026-08-16: `DecideAssurance` now combines client minimum, requested ACR, +provider default, `max_age`, `prompt=login`, and current KeyCape login-session +level. An AAL1 session cannot satisfy an AAL2 client or `acr_values=aal2`. +`coulomb-social` in `config/dev-config.yaml` is `mfaRequired: false`; other +clients keep the provider default. Users without an enrolled factor are sent +to the client's `enrollmentUrl` instead of completing authorization. + +## T03 - Support explicit step-up and fresh authentication + +```task +id: KEY-WP-0008-T03 +status: done +priority: high +state_hub_task_id: "f7acbc12-e597-5d1e-8b29-bfa02237730f" +``` + +Implement supported ACR/max-age or equivalent IAM Profile parameters, invoke +privacyIDEA only when policy requires it, and return verifiable assurance +claims. Handle users without an enrolled factor through a safe enrollment +handoff rather than an authorization bypass. + +Done when coulomb.social can request MFA for a profile/action and verify the +result from token claims. + +Implemented `acr_values` preservation and AAL2/MFA forcing through the +privacyIDEA challenge. The existing token `assurance` claim reports `aal2` +and `mfa: true` only after successful verification. + +## T04 - Prove policy isolation and compatibility + +```task +id: KEY-WP-0008-T04 +status: done +priority: high +state_hub_task_id: "bc660767-387e-59a5-bfce-caccf4082c08" +``` + +Test known and unknown users, registration link eligibility, state expiry and +replay, password-only coulomb-social login, profile/action step-up, no-factor +enrollment, OpenBao mandatory MFA, cross-client session reuse, and logout. +Keep static client registration and exact redirect rules unchanged. + +Done when existing high-assurance clients pass unchanged and the new +coulomb-social journey passes live. + +2026-08-16: isolation tests cover known/unknown users, registration +eligibility, state expiry/replay, password-only coulomb-social, ACR step-up, +no-factor enrollment handoff, OpenBao mandatory MFA, cross-client AAL1 +session reuse, logout, and exact redirect enforcement. Full Go suite passes. +Live coulomb-social AAL1/AAL2 isolation was already proven on railiance01 +under NK-WP-0025-T05 (2026-08-14); this closeout adds the KeyCape-side +regression suite and `/logout`. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md new file mode 100644 index 00000000..c9e585db --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md @@ -0,0 +1,138 @@ +--- +id: KEY-WP-0009 +type: workplan +title: "Provider capability declarations and bounded service identities" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-08-23" +updated: "2026-09-27" +depends_on: + - NK-WP-0030 + - KEY-WP-0006 +state_hub_workstream_id: "c1a9b1cc-2ff0-566a-b544-1a2ef967fc0d" +--- + +# KEY-WP-0009 — Provider capabilities and bounded service identities + +Publish KeyCape-owned security-scenario interfaces and make the existing +service-token issuer precise enough for OpenBao machine-login consumers. This +work accepts identity issuance ownership without taking over OpenBao roles, +policies, secret custody, or privacyIDEA token lifecycle. + +## Publish C1 and C2b provider declarations + +```task +id: KEY-WP-0009-T01 +status: done +priority: high +state_hub_task_id: "eaf7af48-b3d6-5f85-b0ce-ff2b640f9cc3" +``` + +Publish Playbook Capability Contract v0.1 declarations for the KeyCape C1 +runtime and its privacyIDEA-backed C2b integration. Name exact entry points, +parameter authority, resource ownership, trust requirements, and readiness +evidence. Do not claim C2a or privacyIDEA-owned token lifecycle. + +Published `capabilities/playbooks/key-cape.lightweight-sso.yaml` and +`capabilities/playbooks/key-cape.privacyidea-token-authority.yaml`. The latter +claims the KeyCape integration for C2b while leaving factor enrollment, token +state, custody keys, validation decisions, and lifecycle with privacyIDEA. + +## Define bounded service-auth contracts + +```task +id: KEY-WP-0009-T02 +status: done +priority: high +state_hub_task_id: "212eb75c-a36c-5cb8-bf67-7d524f00f9ef" +``` + +Define the KeyCape service-auth claims, renewal/expiry, failure, custody, and +owner boundaries required by secrets-engine and OpenBao JWT roles. Add +per-client access-token lifetimes so a bounded client contract does not depend +on an unrelated global default. + +Added a validated 1m-1h per-client `tokenLifetime` override, applied it to JWT +`exp` and `expires_in`, and documented claims, renewal, expiry, residual JWT +validity, explicit OpenBao cleanup, and no-fallback failure semantics in +`docs/openbao-service-auth-contract.md`. + +## Accept coding-agent issuance ownership + +```task +id: KEY-WP-0009-T03 +status: done +priority: high +state_hub_task_id: "c41e0144-8169-57cf-8c34-eb52e65d6d76" +``` + +Publish the non-secret static registration for `codex-railiance-platform` with +the exact audience, subject, tenant, role, scope, and 15-minute lifetime already +accepted by railiance-platform. KeyCape owns JWT issuance and client disablement; +railiance-platform owns the exact-bound OpenBao role and policy; OpenBao owns +resulting token enforcement; secret values remain outside this repository. + +Accepted ownership through the exact non-secret registration in +`config/service-clients.example.yaml`. Added the parallel reviewed +`secrets-engine-openbao` contract. Live value generation/materialization, +deployment merge, and OpenBao role/policy application remain with their named +custody and platform owners and are not implied by this source registration. + +## Validate and hand off + +```task +id: KEY-WP-0009-T04 +status: done +priority: high +state_hub_task_id: "6945c0c0-bbb6-552b-8d62-d806d3fd0018" +``` + +Run the canonical declaration validator, Go formatting/build/vet/tests, and +repository checks. Reply to NetKingdom, secrets-engine, ops-warden, and +railiance-platform with revision-independent paths and exact ownership status. + +Both declarations pass NetKingdom's canonical validator. The full Go suite, +vet, build, `gofmt`, YAML parsing, and `git diff --check` pass using an explicit +Go 1.23 toolchain and writable local cache; the workstation's default Go cache +is read-only and was not used. + +2026-09-05 correction: the earlier statement that State Hub handoffs were +delivered was not supported by receipt IDs. The audit found no matching handoff +receipts in the Hub response. KEY-WP-0014-T02 records the correction. + +2026-09-08 closure. Verification re-run on `main` with `GOTOOLCHAIN=go1.23.0` and +a writable `GOCACHE`: `make contract-test` PASS, `go build ./...`, `go vet ./...` +and `go test ./...` all pass, `git diff --check` clean. `gofmt` had drifted on +`src/internal/server/telemetry/events.go` and `src/tests/migration/scenario_c_test.go` +(comment alignment only, no semantic change); both were reformatted so the +formatting claim now holds. + +The four handoffs were then actually sent, and each receipt is readable back via +`GET /messages/?from_agent=key-cape`: + +| Recipient | Receipt id | +| --- | --- | +| netkingdom | `692ed0d7-e21b-48e7-8600-5b32b551c627` | +| secrets-engine | `479a1698-87dc-4ee2-a732-c2dc18bc4e84` | +| ops-warden | `5bc58847-eed5-4164-bb0f-a0d8455e75b8` | +| railiance-platform | `9c6a4e70-6bc0-4ad6-8293-35a56e507dfc` | + +Each names revision-independent paths, the 2026-09-08 verification result, and +the same ownership split: KeyCape owns issuance, static registrations, client +disablement and the claim set; secret custody, OpenBao roles/policies and token +enforcement, privacyIDEA factor and token lifecycle, resource authorization and +deployment remain with their named owners. C2a is not claimed. This closes the +notification gap; it is still not evidence of live deployment or provider cutover. + +### 2026-09-27 operator tenant-zero correction + +OpenBao infrastructure identities belong to `tenant:platform`, not Coulomb. +Corrected both static infrastructure registrations, matching source tests and +the tenant/OpenBao contracts. Consumer preflight and platform role sources are +updated together; no live registration or token migration is claimed. The +provider contract remains complete; RPF-WP-0035-T02 and SECRETS-WP-0008-T06 +retain coordinated custody/deployment and native rejection/revocation proof. +Human fallback and workload tenant grants are not elevated to platform. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md new file mode 100644 index 00000000..81614eeb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md @@ -0,0 +1,33 @@ +--- +id: KEY-WP-0010 +type: workplan +title: "Admit the operator-tunneled OpenBao callback" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-08-23" +updated: "2026-08-23" +depends_on: + - RAILIANCE-WP-0027 +state_hub_workstream_id: "8bdf9a24-9dc1-53f9-9448-3b29c36d9950" +--- + +# KEY-WP-0010 — OpenBao operator loopback callback + +## Admit and validate the exact callback + +```task +id: KEY-WP-0010-T01 +status: done +priority: high +state_hub_task_id: "46d78907-bda4-5cc9-853d-98c0026d9fe1" +``` + +Added the exact operator-only callback +`http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback` to the +source-owned `openbao-admin` static client in `config/dev-config.yaml`. The +registration retains exact-match redirects and does not authorize a login, +change the OpenBao role, broaden a listener, or handle a credential. A config +test pins the exact URI and proves the complete source config validates. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md new file mode 100644 index 00000000..f7b906f5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md @@ -0,0 +1,68 @@ +--- +id: KEY-WP-0011 +type: workplan +title: "Recover from live KeyCape Secret output exposure" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-08-23" +updated: "2026-08-23" +state_hub_workstream_id: "158aa957-8818-52ea-b7a5-3962cf30ba5e" +--- + +# KEY-WP-0011 — Live KeyCape Secret exposure recovery + +Recover safely after the base64 data map of Kubernetes Secret +`sso/keycape-config` was mistakenly rendered in agent tool output during a +metadata check. Never reproduce, decode, or store the exposed payload. + +## Contain and notify owners + +```task +id: KEY-WP-0011-T01 +status: done +priority: high +state_hub_task_id: "a8f5e33d-c725-5181-849e-a42bba34d3d8" +``` + +Stopped live rollout work, treated the payload as exposed, and notified +railiance-platform and NetKingdom without values. The affected classes are the +KeyCape RS256 signing private key and credentials embedded in `config.yaml` +(LLDAP bind credential, Authelia client credential, and privacyIDEA admin +token). No value was decoded, reused, or intentionally repeated and no live +mutation was attempted. + +## Approve and execute coordinated rotation + +```task +id: KEY-WP-0011-T02 +status: done +priority: high +state_hub_task_id: "9d60569c-a0f1-53a9-b4dc-01687ab92bcc" +``` + +Approved by the operator on 2026-08-23, accepting deliberate invalidation of +active sessions and a brief SSO interruption. The RS256 key, LLDAP bind/admin +credential, Authelia-KeyCape client credential, and privacyIDEA application +signing secret were replaced without rendering values. The exposed +privacyIDEA admin JWT was removed from live use. Replacement material is +recoverable only through the NetKingdom age recipient in the encrypted +recovery overlay under `history/`. + +## Verify recovery without values + +```task +id: KEY-WP-0011-T03 +status: done +priority: high +state_hub_task_id: "a079573d-97fd-5c6c-b1f9-c0af65e2933a" +``` + +Verified changed resource versions and public JWKS fingerprints, four Ready +deployments, successful replacement LLDAP and Authelia authentication, +wrong-secret rejection, explicit privacyIDEA fail-closed denial, issuer +discovery, and the OpenBao callback handoff to Authelia. The temporary LLDAP +force-reset flag was removed. No Kubernetes Secret payload or authentication +value was rendered during recovery. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md new file mode 100644 index 00000000..0f3deb62 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md @@ -0,0 +1,57 @@ +--- +id: KEY-WP-0012 +type: workplan +title: "Repair UserInfo canonical subject resolution" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: userinfo-canonical-subject-resolution +created: "2026-08-31" +updated: "2026-09-01" +state_hub_workstream_id: "2aca83d8-4e80-52c6-8929-f1e957576b47" +--- + +## Repair subject lookup + +```task +id: KEY-WP-0012-T01 +status: done +priority: high +state_hub_task_id: "bc1ee2c7-a321-5914-96d1-9cb871f0ee1b" +``` + +Resolve the canonical LDAP-DN `sub` emitted by the token endpoint without +passing it to the username-only repository lookup. Preserve stable subject +semantics and verify any `preferred_username` lookup against the canonical ID. + +## Regression verification + +```task +id: KEY-WP-0012-T02 +status: done +priority: high +state_hub_task_id: "7073cc30-a518-5c27-acaf-1a323fd981c6" +``` + +Cover canonical-ID, legacy username-sub, missing subject, and suspended-user +behavior. Run the KeyCape test suite and image build checks. + +## Deploy and verify OpenBao OIDC + +```task +id: KEY-WP-0012-T03 +status: done +priority: high +state_hub_task_id: "571c5237-93fa-5c61-87af-974e02fd8929" +``` + +Publish and deploy the corrected KeyCape image, prove `/userinfo` accepts a +fresh human access token, then resume the governed Policy Nexus bootstrap. + +Completed 2026-09-01. Forgejo Actions run 14 built commit +`153258b9d39273d53ca7ab5d1bdc09961ea9026e`; image digest +`sha256:4c7193efc3e50bd72a01812a583e823cc04f41ca96ef387b31ba9ae2533b80d0` +rolled out successfully. A fresh OpenBao OIDC flow passed `/userinfo`, and the +governed Policy Nexus bootstrap subsequently completed with deterministic +session revocation. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md new file mode 100644 index 00000000..7d77aee3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md @@ -0,0 +1,573 @@ +--- +id: KEY-WP-0013 +type: workplan +title: "Approval-engine resource audience and client registrations" +domain: infotech +repo: key-cape +status: finished +flavor: implementation +owner: codex +topic_slug: approval-engine-resource-audience +created: "2026-09-05" +updated: "2026-09-30" +state_hub_workstream_id: "6e815d88-b0e3-5ce0-be5d-13ab15917f7f" +--- + +Source: approval-engine inbox request 5583e896-52f2-45bd-895f-02f227b7e836, +reviewed against its local registration and caller-authentication contracts. + +## Implement static resource audiences + +```task +id: KEY-WP-0013-T01 +status: done +priority: high +state_hub_task_id: "a92432a2-4e92-5b90-be2b-d82b784bf8f0" +``` + +Added optional static audience configuration for access tokens in both grants; +ID tokens retain the relying-party audience. Added human access-token scope. +Published bounded approval service fragments and the human registration contract. +Regression tests cover the default audience, request override resistance, JWKS +signature validation, ID-token separation and service registration scope isolation. +Browser requests and token exchanges now enforce the client scope allow-list, +including grants removed after authorization. + +## Provision and prove live registrations + +```task +id: KEY-WP-0013-T02 +status: done +priority: high +state_hub_task_id: "607897c5-bad9-55e5-86df-7802f592d6e8" +``` + +Needs deployment-owned custody for both new service secret references and the +compatible image/configuration rollout. The upstream issuer is now verified and +pinned by NetKingdom (2026-09-09). Deploy the implementation and service registrations together, +then prove live JWKS verification and denied excess scopes without logging values. +Local signature proof is not live rollout evidence. See docs/approval-engine-auth-contract.md. +The separate human UI callback gate is retained in T05; a bearer-only resource +server does not own a redirect and its absence does not block these service clients. + + +2026-09-05 follow-up: read-only deployment metadata shows the current image is +forgejo.coulomb.social/coulomb/key-cape:main-153258b and only the Qonto service +secret environment reference is present. The two approval clients are not +materialized through deployment environment references. Published a concrete +non-secret admission packet at docs/approval-engine-provisioning-request.yaml. +Custody routing has no exact admitted lane for these two clients. `warden plan` +returned founder_required but matched an unrelated generic database lane; that +mismatch is not authority to provision. Human callback clarification is pending. +No secrets were read or production resources changed. + +2026-09-08: the two blocking requests were sent and their receipts are readable +back via `GET /messages/?from_agent=key-cape`. + +- `278a3ebe-b529-49f6-bd1a-e3ebcf318260` -> railiance-platform. Custody admission + for both clients, quoting `docs/approval-engine-provisioning-request.yaml` + verbatim: exact OpenBao paths and `client_secret` field, the two Kubernetes + delivery references, the two `KEYCAPE_*_CLIENT_SECRET` environment names, + `tenant:platform` per decision `5ed3fb35-eca9-413a-82b9-95171ba85bf6`, and the + 15m lifetime. It asks for four things explicitly: confirm/correct the paths and + fields, confirm the delivery references, name the authority for the attended + first provision, and agree a rollout window in which registrations and the + KeyCape version that reads them deploy together. It restates that the + `warden plan` `founder_required` result matched an unrelated generic database + lane and is not being treated as authorization. +- `43fbd997-6e22-4859-8433-58b784c2df6c` -> approval-engine. Requests the two + missing human-registration inputs — the exact `client_id` and the full callback + URI — noting redirects are matched exactly and a near-miss fails closed at + `/authorize`. Restates the human client shape (public, S256 PKCE, + `audience: approval-engine`, `allowedScopes: [openid, approval:approve]`, + `mfaRequired: true`, no consume grant) and asks approval-engine to confirm it + validates the access token, not the ID token, against the deployed `/jwks`. + +Deployment state is unchanged from the 2026-09-05 observation: neither approval +client is materialized. Task remains `wait` — the two admissions are owned by +railiance-platform and approval-engine respectively, and live provisioning proof +cannot begin until both land. Still no secret read and no production resource +changed. + +2026-09-08, second pass. Custody and the human callback are still owed, but one +part of this task was ours all along and was not built: the task requires proving +"live JWKS verification and denied excess scopes without logging values", and +there was no way to do that except by hand, against production, at the moment +custody lands — which is the worst time to be improvising a check. + +Shipped `keycape verify-client` (`src/internal/authclient/verify.go`, documented +in `docs/native-authentication.md`). Per registration it verifies discovery +origin, the `client_credentials` exchange and its RS256 signature against the +deployed JWKS, exact `sub`/`tenant`/`roles`/`principal_type`, that every +`-deny-scope` is refused, and that the token carries no scope that was not +requested — the last being a gap the caller commands do not cover, since they +prove requested scopes were granted and not that nothing extra came back. +Failures name the claim, never the observed value, and nothing is written to +disk, so it is safe to run against production. The exact invocation for each of +the two clients is now recorded in the `verification:` block of +`docs/approval-engine-provisioning-request.yaml`, so admission hands back a +command rather than a description. + +Tests: `src/internal/authclient/verify_test.go` covers the passing case, an +over-broad registration, a live predecessor secret, an identical "rotation" and +output non-disclosure. Verified with teeth — neutering `mustFail` makes the suite +fail rather than pass silently. + +Task stays `wait`. What is owed from elsewhere is unchanged and unreduced: the +two secret values through an admitted custody path, and the exact human +`client_id` and callback URI. Nothing here provisions anything. + +### 2026-09-08 attended upstream issuer proof — T02 return + +The user explicitly admitted the prepared temporary probe. At 21:44:44 UTC its +actual Authelia authorization-code exchange verified the signed issuer +**`https://auth.coulomb.social`**, including signature, keycape audience, validity +window and browser-bound nonce. The pinned Job exited 0. No token or user claim +was retained, and no downstream credential was issued. + +All five temporary resources and the Pod were removed with scoped cleanup; +KeyCape Deployment and config Secret metadata remained unchanged. Receipt: +`docs/evidence/2026-09-08-upstream-issuer-live-proof.json`. + +This closes the actual-token issuer observation gate. T02 remains wait for the +configuration owner to ensure `authelia.issuer` is pinned to the verified HTTPS +value, named CCR-2026-0017/0018 reviews, custody delivery and compatible rollout. +The diagnostic did not prove downstream KeyCape MFA/application login and did +not activate custody. Do not repeat discovery or request another observation +unless the provider/configuration or proof context changes. + + +2026-09-09 configuration-owner return: NetKingdom's exercised, revision-guarded +issuer-only helper inserted `authelia.issuer: https://auth.coulomb.social` into +`sso/keycape-config`; resourceVersion `51346058` -> `58713343`. Independent +readback matches. Every other configuration byte and Secret data entry, including +the private key, was preserved. Thirteen tests passed. Receipt: +`docs/evidence/2026-09-09-upstream-issuer-pin.json`. + +The stored issuer configuration prerequisite is closed. No process restart, +image rollout, custody activation or downstream login proof occurred. T02 stays +wait for the named CCR reviews, attended custody and compatible rollout. The +single-instance replacement and existing-human-login checks remain mandatory. + +2026-09-09 completion: both named CCR reviews are recorded. Verifier-side custody, +ESO delivery and the compatible pinned image/config are now live. Both service +clients passed the pinned verifier plus independent live signature and exact +claim/lifetime checks; wrong-secret and excess-scope requests were rejected. +The existing OpenBao human login passed before and after replacement; each +attended session self-revoked. Receipt: `docs/evidence/2026-09-09-keycape-verifier-admission.json`. + +The deployment owner exercised compatible rollback and version-preserving resume. +The final helper checks containerd imageID against the pinned manifest and uses +the existing 30-second future-iat bound without extending token expiry. The +pinned native verifier runs in the contained owner process because workload +network policy excludes public issuer HTTPS. No network grant was broadened. + +Live residual ownership remains KEY-WP-0013-T05 for the approval UI callback/MFA; +RPF-WP-0035-T05 for separate client-side delivery; AUDIT-WP-0009-T09 for audit +custody; APPROVAL-WP-0002 and SECRETS-WP-0009-T03 for end-to-end claim/consume, +expiry/revocation and native delivery. Natural JWT expiry and real predecessor +rotation were not exercised by initial provisioning. + +## Reconcile tenant vocabularies across approval layers + +```task +id: KEY-WP-0013-T03 +status: done +priority: high +state_hub_task_id: "e98da2eb-1f05-5c7f-bc84-711fa38b8c4a" +``` + +Source: glas-harness inbox message 356f6977-d361-4e3b-83ab-b2c7f4759286 +(GLAS-WP-0015), which asks for the exact store tenant, JWT tenant, CheckRequest +tenant, any permitted mapping and wrong-tenant denial evidence. + +Published `docs/tenant-claim-contract.md`. The KeyCape-owned JWT tenant for all +four reviewed service registrations is `tenant:coulomb`, bound at registration +and required by config validation. Approval store `platform` and policy +`tenant:platform` are owned by approval-engine and flex-auth; KeyCape performs no +normalization or aliasing, so exact comparison does not match today. No mapping +was invented and no live registration or policy subject was changed — the two +admissible resolutions are recorded for the owning parties to decide. + +Added `src/internal/server/oidc/tenant_test.go`: request-supplied `tenant` and +`tenant_hint` cannot alter the claim; two registrations never carry each other's +tenant (the wrong-tenant denial basis); human tokens default to `tenant:coulomb` +rather than an empty claim. Local issuance proof only, not live-rollout evidence. + +## Align approval registrations to tenant:platform + +```task +id: KEY-WP-0013-T04 +status: done +priority: high +state_hub_task_id: "d93e21cc-a422-5e78-981f-9c00f3161375" +``` + +Source: glas-harness inbox message f487c63a-dd7e-4ad8-9188-eec9fbea59c6, +operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 ("Use tenant:platform for +the Glas approval dependency chain", verified resolved in the hub). This closed +resolution 2 of the two options recorded in KEY-WP-0013-T03. + +Changed the `tenant` field on exactly `secrets-engine-approval` and +`approval-engine-operator` in `config/service-clients.example.yaml` and the two +matching entries in `docs/approval-engine-provisioning-request.yaml` to +`tenant:platform`, each annotated with the decision id. `codex-railiance-platform`, +`secrets-engine-openbao` and the human directory default keep `tenant:coulomb`. +No audience, scope, subject, role, lifetime or MFA change; no cross-tenant grant. + +Evidence: `TestApprovalClientIssuesExactPlatformTenantAndRejectsAliases` (exact +`tenant:platform` with `aud=approval-engine` even when the caller requests +`platform`, `tenant:coulomb` or `TENANT:PLATFORM`), +`TestUnrelatedServiceClientKeepsCoulombTenant`, and +`TestServiceRegistrationTenantsAreExactPerDecision`, which pins every reviewed +client's tenant against the real fixture so a reintroduced alias fails the build. +Full `go test ./...` and `go vet ./...` pass. Choice only — live provisioning and +the other admission gates remain with KEY-WP-0013-T02. + +2026-09-09 observation, read-only. Custody has been admitted since the +2026-09-08 record and the deployment now carries the pieces T02 was waiting for: +`sso/keycape-config` registers `secrets-engine-approval` and +`approval-engine-operator` alongside the existing clients; the secrets +`keycape-secrets-engine-approval-client` and +`keycape-approval-engine-operator-client` exist in the namespace; both +`KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET` and +`KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET` are wired into the pod; and the +image is pinned by digest to +`sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`, the +candidate T06 published. + +What that does and does not establish. The live issuer passes every conformance +check that needs no credential — it advertises itself, offers the profile +authorization surface, publishes usable RS256 keys and advertises no excluded +grant (`src/tests/conformance`, run against `https://kc.coulomb.social`). It does +**not** establish the claim contract: subject, tenant, roles, scopes, lifetime +and excess-scope denial per client are exactly what `keycape verify-client` +exists to prove, and that needs the client secret in the environment. + +Deliberately not done here. Reading either secret to run the verifier is +client-side retrieval, which the provisioning packet records as **not admitted** +(`custody_return.scope`: verifier-side copies only). So the remaining proof is an +attended operator action, not something to take by reading the secret. T02 stays +`wait` on that run, not on custody. + +**Correction, same day.** The refusal above was right; the conclusion drawn from +it was not. The attended run had already happened, on 2026-09-09T00:10Z, and its +receipt is committed in this repository at +`docs/evidence/2026-09-09-keycape-verifier-admission.json`. Per client — both +`secrets-engine-approval` and `approval-engine-operator` — +`live_jwks_signature_verified: true`, `exact_claims_verified: true`, +`excess_scope_denied: true`, `wrong_secret_denied: true`, +`lifetime_seconds: 900`, `maximum_future_iat_seconds: 30`, +`pinned_artifact_verifier_passed: true`, `verifier_location: "attended owner +process"`. `credential_values_emitted: false` and +`client_side_read_admitted: false` throughout, so it was run the admitted way. +The claim contract is established and T02's `status: done` is correct; it does +not revert to `wait`. + +The reasoning error is worth naming because it is the third of its kind here in +two days: "not admitted **for me**" was read as "not done **by anyone**", without +checking `docs/evidence/`. The preceding paragraph's read-only observations are +sound and its restraint about not reading the secret is exactly right — only the +final sentence was wrong, and it is retained above rather than deleted so the +correction has something to point at. + +What the receipt itself declines to claim, which is the honest residual and +matches this task's earlier record: `real_predecessor_rotation_tested: false`, +`observed_wall_clock_expiry: false`, and under `limits`, +`actual_predecessor_rotation_observed: false` and +`wall_clock_jwt_expiry_observed: false`. Natural expiry and a real predecessor +rotation were not exercised by initial provisioning and are not claimed by +anyone. Predecessor rejection is implemented and unit-tested in +`keycape verify-client`; it has not been run against a real rotation, which is +KEY-WP-0014-T04's territory and still has no admitted execution authority. + +**Independently confirmed by the custody owner, 2026-09-09** (railiance-platform +`ae6ff4bc`, `570e6746`). They reached the same reading from their own side and +add one datum this record did not carry: `human_client_consume_denied: true` for +both clients, alongside the signature, exact-claim, lifetime, excess-scope and +wrong-secret results. They confirm the verifier ran twice — once during +activation and again after rollout — at deployment generation 38, single ready +replica, signing key unchanged, no credential values emitted, and state that T02 +can be closed against that receipt rather than against a run KeyCape cannot make. + +They also correct one thing in our framing, and they are right: the packet says +*client-side retrieval* is unadmitted, which remains true, but the attended +operator path is not a client-side read and never required one. The two are +distinct and this task had been treating them as the same constraint. + +**Do not cite this receipt as rotation evidence.** Both owners now state the same +two gaps independently: `real_predecessor_rotation_tested: false` and +`observed_wall_clock_expiry: false`. Neither is a T02 acceptance criterion, and +neither is closed by anything recorded here — see KEY-WP-0014-T04. + +**Client-side retrieval is prepared but still unadmitted.** CCR-2026-0019 +(secrets-engine reader) and CCR-2026-0020 (approval-engine-operator reader) are +`in_flight` under RPF-WP-0035-T06. 0020 has no named presenting actor in any +owner source, and railiance-platform left it undetermined rather than guessing — +correctly. KeyCape's view, asked for as the issuer of the identity and recorded +in `docs/approval-engine-provisioning-request.yaml`, is that the registration as +issued carries both `approval:create` and `approval:approve`, so a single +presenter can create and then approve the same entry. That is a +separation-of-duties question about who may hold the credential, not a defect in +the token: KeyCape issues the grants approval-engine requested. The decision is +approval-engine's and the doctrine question is gate-house's. + +**Answered 2026-09-09, and the answer withdrew the client.** approval-engine +checked and the presenter is nobody (`056aef0d`). Nothing in their repository +obtains an OAuth token — the engine only verifies them, there is no +client-credentials grant in the package, and the one outbound credential is a +mounted audit-token, not an OAuth client. Scope by scope the bundle never +described a single actor: `approval:approve` now belongs to the informed-decision +human client, `approval:emit` is redundant because the running server emits its +own heartbeat with no token, and no requester identity was ever settled for +`approval:create`. It was a convenience bundle written when a lifecycle operator +was assumed to exist. + +They asked railiance-platform to cancel the client-side reader CCR-2026-0020 as +an owner decision. Verifier custody CCR-2026-0018 stands, so the client remains +authenticable if a presenter appears, and they asked for it to be kept on that +basis rather than removed. Annotated as withdrawn in +`config/service-clients.example.yaml` and the provisioning packet; not deleted. +The separation-of-duties observation above is retained because it constrains the +re-request: when a presenter is named the registration comes back **narrowed**, +and `approval:approve` must not travel with the operational scopes. + +**One thing "do not provision" cannot undo, raised rather than filed.** This +client is already live — provisioned in the 2026-09-09 attended rollout and +verified in `docs/evidence/2026-09-09-keycape-verifier-admission.json`. So the +estate now holds a confidential credential that can create *and* approve +approvals, that nobody presents, and that no consumer is waiting on. That is +standing capability with no counterparty, which is a worse resting state than +either provisioning it for a named presenter or disabling it. KeyCape owns client +disablement, so the action is ours to take and not ours to decide alone; raised +with approval-engine and railiance-platform rather than exercised, since removing +a live registration is a deployment change. Recorded here so it is not lost: +**an unpresented create+approve credential should not be left standing +indefinitely.** + +## Assign and register the human approver browser client + +```task +id: KEY-WP-0013-T05 +status: done +priority: high +assignee: the-custodian +state_hub_task_id: "9a782909-91db-59fa-aae7-83766f4fbb0d" +``` + +Own the residual human registration separately from service-client T02. Reuse +an existing accepted UI if available; do not invent a callback on approval-engine. +Require public S256 PKCE, exact redirect, MFA, approval:approve without consume, +and a real human access-token proof. No service credential substitutes for this. +HFACT-WP-0001-T03 consumes the acceptance where a human approval is required. + +2026-09-09. The owner exists: `informed-decision` (hub repo cf4c7da8) claims the +approver surface and owns the two strings. approval-engine was right that a +bearer-only resource server never owned them. Both wrote in asking to be told +early about problems with the registration shape rather than at handover, so +their requested shape was checked against the source rather than agreed on +paper. Three results. + +**Scope gap — accepted, theirs was right.** `[openid, approval:approve]` cannot +render a decision: `GET /v1/approvals/{id}` and `/claim` both need +`approval:read`, so the surface could submit an entry it was never able to +display. Published `[openid, approval:read, approval:approve]` in +`docs/approval-engine-auth-contract.md` and the provisioning packet. Reading +through a service identity was the alternative and is worse: it weakens the +evidence-of-what-this-person-saw claim the component exists to make. +`approval:consume` stays excluded. + +**Assurance shape — published, and a defect fixed.** Both asked for a documented +shape; KeyCape already emitted one, so it is written down rather than +renegotiated: `level` (aal1|aal2, ~acr), `methods` (~amr), `mfa`, `source`, `at`. +Writing it down surfaced that `at` was the token **mint** time, not the +authentication time. Those differ by hours whenever a browser session is reused, +and approval-engine persists this object verbatim as the only downstream record +that MFA happened — so a stored approval could have evidenced MFA at a moment the +person proved nothing. `PKCESession.AuthTime` now carries the original login +instant through session reuse, with mint time as the fallback for sessions +predating the field. Covered by `src/internal/server/oidc/assurance_time_test.go`. + +**Blocker found before anyone built on it — a human token cannot carry +`tenant:platform`.** `effectiveTenant` resolves the human tenant from the +directory user, not the client registration, and no adapter populates +`domain.User.Tenant`, so every human token defaults to `tenant:coulomb`. The +per-client `tenant` field that carries `tenant:platform` on the two service +clients is read only on the `client_credentials` path. approval-engine compares +tenant by exact string equality and pins near-miss spellings as refused, so an +approver token issued today would be rejected — and it would present as a failed +approval, not as a registration defect. + +Two resolutions, sent to both owners (`67132ddc`, `a6a070a1`, pointer +`42d435e8`) rather than chosen here, because the choice decides what a human's +tenant *means*: (1) directory-sourced, approvers carry `tenant:platform` on their +LLDAP record — keeps tenant a property of the person, needs a directory attribute +and an owner, and changes those users everywhere; (2) registration-bound and +fail-closed, symmetric with the service clients and with decision +`5ed3fb35-eca9-413a-82b9-95171ba85bf6` — defensible only because registrations +are static and deployment-owned, since dynamic registration is excluded by +design. KeyCape leans to (2) and deliberately implemented neither: (1) is a +directory-ownership question and (2) writes a cross-tenant capability into the +issuer. + +**Tenant blocker resolved in code, 2026-09-09.** On review the choice did not +have to be made to unblock: the two resolutions differ in where a human's tenant +*comes from*, and an implementation exists that is correct under either. A client +registration may now declare a `tenant`, and `humanTenant()` resolves it by four +rules — no declaration keeps the directory answer unchanged; a declared zone +applies when the directory has placed the user nowhere; agreement passes; and a +declared zone that **conflicts** with a directory assignment refuses issuance +(`403`, `error_type: tenant_binding`) rather than relabelling the user. + +That refusal is the whole design. A registration can bind a zone for unplaced +users and can never move a placed one, so option (2) is available now without +writing a general cross-tenant override into the issuer, and if the owners prefer +option (1) the same code stops supplying the zone and starts enforcing agreement +with it — no second migration and no window where a stale registration silently +wins. It is safe only because registrations are static and deployment-owned; +`docs/tenant-claim-contract.md` records that this rule must be revisited if +dynamic client registration is ever admitted. + +Evidence: `src/internal/server/oidc/human_tenant_test.go`, four cases including +the relabel refusal. Verified with teeth — neutering the conflict check makes +`TestHumanTenantRefusesToRelabelAPlacedUser` fail rather than pass silently. +`docs/tenant-claim-contract.md` carries the resolution table, +`docs/approval-engine-auth-contract.md` states the requirement on the approver +client, and the provisioning packet now declares `tenant: tenant:platform`. + +**Cleared and registered 2026-09-10.** informed-decision submitted the two +strings with the origin already live: + + client_id informed-decision-approver + redirect_uri https://decisions.coulomb.social/auth/callback + +The host is `decisions.coulomb.social`, not the `decide.coulomb.social` of an +earlier draft. They verified the origin rather than reporting it: `/` and +`/auth/callback` both return 200 from 92.205.62.239 on a Let's Encrypt +certificate CN=decisions.coulomb.social valid 2026-09-10 to 2026-12-09. The path +serves a placeholder while their surface is gated on APPROVAL-WP-0002-T01, which +does not affect the registration — the redirect is matched as an exact string at +`/authorize` and never fetched. + +Published in `config/service-clients.example.yaml`: public `authorization_code` +with S256 PKCE, `audience: approval-engine`, scopes +`[openid, approval:read, approval:approve]`, `mfaRequired: true`, declared +`tenant: tenant:platform`. No `secretRef` — PKCE is the whole proof — and no +`serviceSubject` or `roles`, which validation rejects on a browser client. + +`TestApproverRegistrationShapeIsExact` pins every field, so widening a scope or +relaxing MFA fails the build rather than reading as an edit, and asserts the +registration passes startup validation — which proves the KEY-WP-0028 tenant +exemption holds for the registration that depends on it, not only in a synthetic +case. + +Two existing guards fired on the way in, both correctly, and neither was loosened: + +- `TestServiceRegistrationTenantsAreExactPerDecision` refused an unreviewed client + carrying a tenant, which is exactly its purpose. The approver was added to its + reviewed set as a deliberate act. +- `TestServiceRegistrationAudienceAndScopeIsolation` panicked on + `secretRef[4:]`, an assumption that was safe while the fixture held only + confidential clients. The approver is the first public one; the loop now guards + on the `env:` prefix. + +The tenant is declared, and the token says so: `tenant_source` is `registration`, +never `directory`. Per gate-house's correction that is adequate for +approval-engine's store-isolation gate and **never** for doctrine turning on this +person's membership of the zone. + +Not live: this is a published registration fragment. Deployment is owner-side and +nothing has been issued to a real approver. + +## Make negative rollout evidence discriminate actual issuer refusal + +```task +id: KEY-WP-0013-T06 +status: done +priority: high +assignee: the-custodian +state_hub_task_id: "65132e1d-10db-5915-a664-83beaf30365a" +``` + +Critical-path review found `mustFail` accepted any Exchange error: timeout, +HTTP 5xx, invalid token or JWKS failure could falsely prove excess-scope denial +or predecessor rejection after a successful positive exchange. Preserve a bounded +typed provider refusal, require the exact token endpoint/status/error/feature, +and prove the checker rejects unrelated failures without exposing provider bodies. +Publish the verified image candidate and return its immutable digest to T02. + +Platform's 2026-09-08 return is RPF-WP-0035-T05 and proposed CCR-2026-0017/0018. +The custody field is `CLIENT_SECRET`; Kubernetes key `client-secret` and both env +names remain as requested. These records cover verifier-side delivery only. +Owner approval, client-side retrieval and the actual upstream ID-token issuer +proof remain distinct gates. Public discovery currently advertises +`https://auth.coulomb.social`; that alone is not the signed-token observation. + +T06 completion: full Go suite and vet passed; published code `dcebd46` and pulled the image by digest `sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`. Container `keycape verify-client --help` confirms the command is present. `docs/approval-clients-rollout.md` and its proposed deployment patch record configuration/issuer, first-provision, single-instance rollout, acceptance and rollback. This closes verifier preparation only; T02 and T05 retain the explicit live dependencies. + + +## Prepare a bounded upstream issuer observation before client rollout + +```task +id: KEY-WP-0013-T07 +status: done +priority: high +assignee: the-custodian +state_hub_task_id: "62bc6d8e-dbb2-57b6-a30b-472ffd06b497" +``` + +2026-09-08: T02's actual upstream signed-token issuer gate had no executable +observation path. A downstream OpenBao/KeyCape login cannot return the upstream +Authelia token; it remains inside the adapter. Prepare `probe-upstream-issuer` +and an exact-state temporary route so one attended flow can be verified before +changing the normal KeyCape Deployment or its configuration. + +Acceptance: signed issuer/audience/time/nonce proof without token or user-claim +output; browser/state binding and one-shot refusal; finite issuer allowlist; +ten-minute bound; no downstream credential; current Traefik route isolation; +immutable image; reviewed temporary Job/config projection, network policies and +owner-reference cleanup. Normal callbacks and the production issuer stay on +the existing Deployment. The probe mounts only `config.yaml`, not `key.pem`, +and has no Kubernetes API token. That file contains existing credential data; +its use by the temporary diagnostic needs deployment-owner admission. + +Source tests, image and deployment packet close this preparation task. The +attended live receipt and issuer pin remain in T02, alongside the named CCR +reviews and custody/rollout acceptance. Preparing the diagnostic does not +approve CCR-2026-0017/0018 or close any live factory gate. + +T07 preparation complete: source `6f33abd`, immutable image +`sha256:0c85ed377cae7ae6ca5b5c56b1a52930e706b3cb78009747e42f551e869a22e4`. +Complete Go race suite and vet passed; five renderer/collector checks, seven +real Traefik 3.6.7 route cases and Railiance server dry-run passed. The packaged +command ran as UID 65534 with no network and a read-only filesystem and produced +the expected bounded idle-timeout receipt. See `docs/upstream-issuer-proof.md` +and `docs/evidence/upstream-issuer-probe.json`. T02 retains the live proof and +configuration-owner admission; no production resource or custody value changed. + +## Status 2026-09-30 (CUST-WP-0074) + +All tasks T01-T07 are `done`; nothing in this plan is waiting, so it is +`finished` rather than `blocked`. Residual items live in other plans +(RPF-WP-0035-T05/T06, AUDIT-WP-0009-T09, KEY-WP-0014-T04). The one open +question recorded above, disabling the unpresented create+approve client, is a +KeyCape deployment decision to be tracked in a new plan if taken up. + +**2026-10-10:** KeyCape proposed the joint disablement sequence that +railiance-platform asked for on 2026-09-15, to railiance-platform, approval-engine +and net-kingdom: remove the registration (attended), verify `invalid_client`, +unbind the env, retire the ESO delivery, then retire custody. The proposal executes +nothing. Each owner confirms their step and the founder schedules the attended window. + +**2026-10-10, amended by the owners.** net-kingdom: step 1 (`keycape-client.py +apply --operation remove`, case acceptance CCR-2026-0018) only patches +`sso/keycape-config` and does not restart. The step-3 env removal triggers the one +rollout, and the new pod starts with neither the client nor the env. Step 2's +`invalid_client` check follows that rollout. Rollback runs in reverse: env first, +then the client. railiance-platform (RPF-WP-0035-T10): step 4 also retires the +ClusterSecretStore, the Kubernetes-auth role and the reader policies, through an +attended prune sync run strictly after step 3. Step 5 is a metadata delete, not an +archive, run only after the step-2 receipt. Steps 1-5 can share one attended +window. Still open: approval-engine's reconfirmation that there is no presenter, +and the founder scheduling the window. Before that rollout, run +`keycape check-config` on the patched configuration. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md new file mode 100644 index 00000000..bacf04c0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md @@ -0,0 +1,362 @@ +--- +id: KEY-WP-0014 +type: workplan +title: "Review native login and client credential lane handoffs" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: native-credential-lane-handoff +created: "2026-09-05" +updated: "2026-09-10" +state_hub_workstream_id: "0d003df3-f7d3-5063-8ca0-e1e33f7df74a" +--- + +Source: ops-warden inbox message 0dd9c7bd-0ecd-42d1-806f-7cc4ba9730ed. +The native service exchange and public PKCE login commands are now implemented. +Cross-owner rotation and consumer-specific route cutover remain outstanding. + +## Design owner command and custody boundaries + +```task +id: KEY-WP-0014-T01 +status: done +priority: medium +state_hub_task_id: "0c0a0b61-c19e-5631-9cda-8b2dc0f47d8f" +``` + +Review ops-warden's existing key-cape-oidc-login proxy and +rapp-qonto-keycape-client route contracts. Specify the native interactive login +and bounded exchange commands, token delivery, renewal and custody-mediated +rotation before implementation. Keep secret custody with OpenBao and avoid +retiring the proxy until replacement commands have equivalent verification. + +## Verify handoff delivery evidence + +```task +id: KEY-WP-0014-T02 +status: done +priority: low +state_hub_task_id: "d9a5de97-b7d5-5599-98c2-eaab32f51495" +``` + +Ops-warden reports KEY-WP-0009-T04's claimed reply did not arrive. Verify prior +receipts for all four named recipients before claiming successful notification. +No outbound coordination messages were sent during the 2026-09-05 repo review. + + +## Implement and verify native caller commands + +```task +id: KEY-WP-0014-T03 +status: done +priority: high +state_hub_task_id: "4e46f474-a15e-594f-ae07-ad37a9667d89" +``` + +Implemented keycape service-token and login with HTTPS discovery, RS256/JWKS +verification, exact audience bindings, PKCE/state/nonce, private out-of-Git file +delivery and redacted errors. Service Basic authentication now form-decodes +credentials. Integration tests exercise the real token/JWKS handlers and browser +callback, including forged state, tampering, excess scope and unsafe output. +See docs/native-authentication.md for invocation and ownership boundaries. + + +2026-09-10. The authority question is answered and the blocker is narrowed to a +schedulable act. railiance-platform named the executor (platform operator, +attended founder session; explicitly not ops-warden, not secrets-engine +autonomously, and no unattended agent), the transport (the governed +`openbao-platform-admin-login` lane via `warden access ... --exec`), and the +authority (founder_required attended OIDC, role=platform-admin). What is missing +is a rotation CCR, which they will prepare *if KeyCape asks* — a two-custodian +CAS rotation with a service restart being a distinct version-guarded operation +rather than an implementation detail of an existing lane. + +**Not asked, deliberately.** Requesting a production credential rotation with a +restart window is the operator's decision, not this repository's, so it was put +to him rather than answered here (ops-warden msg 10fcc8af). + +Established before recommending anything either way: **this rotation has no +incident driver.** KEY-WP-0011 recovered a real exposure on 2026-08-23, and the +coordinated rotation that followed replaced the RS256 signing key, the LLDAP bind +credential, the Authelia client credential and the privacyIDEA admin token. The +`rapp-qonto` client secret was not in that payload — it is an `env:` secretRef +resolved from a separate Kubernetes Secret, never inline in `config.yaml`, which +config validation enforces. So this is lifecycle hygiene, not remediation, and +nobody should be carrying it as leftover incident work. Hygiene can wait for a +chosen window; remediation could not. + +Also recorded: `warden route`/`warden plan` previously returned a generic match +that read as authorization, and offered read transports against a write need. +This repository declined to act on it; railiance-platform independently called it +a defect; fixed under WARDEN-WP-0038. + +## Admit rotation and verify consumer handoff + +```task +id: KEY-WP-0014-T04 +status: done +priority: high +state_hub_task_id: "e7632c71-9676-5837-a925-7f905e3015c5" +``` + +Qonto rotation requires admitted custody/provider transaction and rollback +semantics; see the exact review sequence in docs/native-authentication.md. +The current login proxy yields an OpenBao token, so it cannot be replaced by a +JWT-writing command without adapting its consumer contract. Verify that split +with the platform owner and ops-warden before route changes. No route was changed. + +2026-09-05 receipt audit: GET /messages/?from_agent=key-cape returned one +2026-03 image-publication message and no KEY-WP-0009 handoff receipts for +NetKingdom, secrets-engine, ops-warden or railiance-platform. Corrected the +unsupported statement in KEY-WP-0009-T04. This proves only that no receipts +are present in the current Hub response, not that no message ever existed. +The 2026-09-05 audit's finding stands as recorded; the replacement outbound +messages were sent on 2026-09-08 (below). + +2026-09-08: `0a6c9d88-cf50-45d8-acdc-99a40747683e` -> ops-warden, readable back +via `GET /messages/?from_agent=key-cape`. It puts both open questions to the +route and custody owner in one place: + +1. The `key-cape-oidc-login` proxy yields an OpenBao token and `keycape login` + yields an issuer JWT — different credential types, different verifiers, + different enforcement owners, so not interchangeable. Two admissible options + are offered: keep the proxy and run `keycape login` alongside it for JWT + consumers, or adapt the route's consumer contract to a JWT and then cut over. + Option (a) is recommended until every consumer of the route is enumerated. +2. Qonto rotation for `rapp-qonto-keycape-client`, quoting the five-step reviewed + sequence from `docs/native-authentication.md` and asking who executes the + successor generation and CAS update, in which transport, under what authority. + It records that `warden route`/`warden plan` returned no exact admitted lane + and that a generic match is not being treated as authorization. + +The KEY-WP-0009 handoff ops-warden reported missing has also now been delivered +(`5bc58847-eed5-4164-bb0f-a0d8455e75b8`), closing the KEY-WP-0014-T02 correction. + +Task remains `wait` — no route was changed and no rotation command ships until +ops-warden answers (1) and an execution/rollback authority is admitted for (2). + +2026-09-08, second pass — read ops-warden's own catalog rather than waiting for a +reply, and it changes the shape of both questions. + +`registry/routing/catalog.yaml`, lane `key-cape-oidc-login`: `intended_owner` was +already corrected to key-cape, and `verified: asked-and-waiting` since +2026-08-28. **ops-warden is waiting on us, not the other way round.** The lane is +also `warden_executes: false` with a `wiki_ref` into `CredentialRouting.md` — a +pointer, per their ADR-0001, not a code path with programmatic consumers. So the +consumer-contract risk behind question (1) is smaller than assumed: adding +`keycape login` alongside retires nothing, and the acceptance we owe them is a +statement of ownership, not a cutover. + +Lane `rapp-qonto-keycape-client`: `rotation.owner: key-cape`, +`automatable: true`, and `blocked_on` reads "still no key-cape-native +exchange/rotation command", `verified: source-read` 2026-08-28. **That blocker is +stale.** `keycape service-token` (T03) shipped 2026-09-05 and is exactly the +native `client_secret_basic` exchange they record as absent. It does not clear +the lane, and should be narrowed rather than closed. + +Shipped here: `keycape verify-client`, which is rotation step 4 and the denial +checks step 5 rests on — new exchange verified against deployed JWKS, predecessor +refused, excess scope refused, no value printed or written. See +`docs/native-authentication.md`, "Verifying a live registration". The identical +predecessor case is treated as a failure, because an unchanged secret is a +rotation that did not happen. + +Steps 1-3 remain custody's and are deliberately not automated. Task stays `wait` +on exactly two answers: which option ops-warden takes for the login proxy, and +who executes the successor generation and CAS update under what authority. No +route changed. + +Both catalog corrections were sent, receipts readable via +`GET /messages/?from_agent=key-cape`: + +- `12f1bdfa-b240-414b-aa49-128a4d2acf20` — accepts ownership of lane + `key-cape-oidc-login`, answering the WARDEN-WP-0033-T04 follow-up they have + carried as `asked-and-waiting` since 2026-08-28. States the boundary the lane's + `fetch_command` crosses: key-cape owns browser authentication and the identity + token; the `netkingdom` auth mount, `role=` mapping, token store and + enforcement of the resulting OpenBao token are not ours. Supplies the source + paths they need to move the lane to `verified: source-read`, and records our + revised, lower estimate of the consumer risk — a pointer lane under ADR-0001 + retires nothing when `keycape login` runs alongside it — while asking them to + correct us if the catalog understates its consumers. +- `08d42f47-8e19-4976-98f5-dd259e7b1225` — reports lane + `rapp-qonto-keycape-client`'s blocker as stale against its 2026-08-28 + source-read, since `keycape service-token` is the native `client_secret_basic` + exchange it records as absent and `keycape verify-client` is its rotation step 3 + verbatim. Asks them to **narrow** it to steps 1-2 rather than clear it, with + proposed replacement text, and notes `rotation.automatable: true` is now + half-true and should say so precisely rather than be flipped either way. + +Neither message asks for a route change and neither was one. + +2026-09-08/09 — ops-warden answered both, and the second answer matters more than +the question did. + +**Item 1 answered: option (a).** Keep the proxy as-is and run `keycape login` +alongside it, "for your reason rather than out of caution -- they are different +credential types with different verifiers, so this is not a cutover deferred, it +is two things that were never one." They verified our pointer-layer reading +rather than accepting it, and added a fact we did not have: `warden access` +already excludes `is_login` lanes from raw-value streaming +(WARDEN-WP-0032-T05). Lane `key-cape-oidc-login` moved to `owner-confirmed` with +our boundary written in verbatim (commit c133004). It stays `interim` on purpose: +per their ADR-0003 a lane retires when a front door exists, not when ownership is +agreed, and by our own analysis `keycape login` is not that front door. The entry +now names the retirement condition instead of implying we owe one. + +**Item 2 answered: nobody owns it, admittedly.** Steps 1-2 mutate custody on +platform workload paths, which is railiance-platform's, with `secrets-engine exec` +as the admitted transport. ops-warden declined to take the act on the grounds +that fronting the read lane is not authority to mutate — their ADR-0002 and +ADR-0005 — and routed it themselves rather than describing the route and leaving +it to us. Lane `rapp-qonto-keycape-client` narrowed exactly as asked and moved +`source-read` -> `owner-confirmed`, crediting the correction to us rather than to +a re-read they did not perform. Rotation step 3 now points at +`keycape verify-client` instead of restating its assertions. + +**They found a defect in their own front door because of our caution, and told +us.** Running our need through `warden plan` returns `verdict: autonomous`, +`lane_id: rapp-qonto-keycape-client`, answered with three read transports — a +need containing *generate* and *CAS-write* inherits the verdict of the lane that +reads the same path, because `warden plan` has no read-versus-mutate intent at +all. `autonomous` is documented as the signal to proceed without the founder. +Raised as WARDEN-WP-0038. Their stated conclusion, which we should not soften: "A +counterparty declining to trust our output is not a control, and we are not going +to record it as one." + +Standing instruction from them until WP-0038 lands, recorded here so it is not +lost in an inbox: **treat a `warden plan` verdict on any need containing a write, +rotate or provision act as unreliable.** That applies to this repository whenever +it plans a rotation. + +**On `automatable`, they did not do what we suggested and said so.** We asked +them not to flip the boolean and to state the half-truth precisely; the schema has +one boolean whose only consumer is a future executable driver, and a driver told +`true` would attempt the custody steps. So it is `false`, failing safe, with the +precision moved into per-step notes. They offered to add a per-step field if we +would rather. We would not: a lane-level flag that fails safe plus per-step notes +is the right shape, and asking them to change a schema for our convenience would +be worse than the imprecision. No reply needed on that point. + +T04 stays `wait`: the rotation authority now has a named owner +(railiance-platform, transport `secrets-engine exec`) but does not yet exist, and +nothing here changed a route. + +2026-09-09, guarding against a misreading that is now easy to make. The approval +clients went live and `docs/evidence/2026-09-09-keycape-verifier-admission.json` +records a passing verifier run, so there is now a committed receipt in this repo +that looks like rotation evidence and is not. It records +`real_predecessor_rotation_tested: false` and `observed_wall_clock_expiry: false`, +and under `limits`, `actual_predecessor_rotation_observed: false` and +`wall_clock_jwt_expiry_observed: false`. railiance-platform stated the same two +gaps unprompted from their side (`ae6ff4bc`): "do not cite this receipt as +rotation evidence." + +So `keycape verify-client`'s predecessor rejection has been exercised only +against unit tests and a first provisioning where predecessor and successor were +never distinct. Rotation step 4 is implemented and unproven, which is a different +state from either "missing" or "done", and T04 is where that distinction lives. + +**2026-09-10: the authority is answered. What remains is an artifact and a +decision.** railiance-platform answered the routed question (`7c7228ac`, relayed +by ops-warden `b14655b3`): + +- **Who:** the platform operator, attended, as a founder-attended session. Not + ops-warden, not secrets-engine autonomously, and no unattended agent — + railiance-platform named themselves in that exclusion too. +- **Transport:** the governed `openbao-platform-admin-login` lane, invoked only + through `warden access openbao-platform-admin-login --exec -- ` with a + unique metadata receipt path. The owner command is never run directly. +- **Authority:** `founder_required` attended OIDC via netkingdom + `role=platform-admin`. Nothing else in that repo carries write authority + against `platform/workloads/`. + +**Missing: a rotation CCR.** They will not raise one speculatively, and their +reason is sound — a two-custodian CAS rotation with a service restart is a +distinct version-guarded operation, not an implementation detail of an existing +lane. It must name the CAS precondition and expected version on *both* +custodians, sibling-field preservation on the KV write, the restart window, +bounded predecessor retention for rollback and denial checks, and the +reconcile-to-same-version step on failure. That last is step 5 of the sequence in +`docs/native-authentication.md`, so the reviewed sequence is what the CCR encodes. + +**Blocked on a decision that is not KeyCape's to take unilaterally.** Their offer: +"if key-cape wants the rotation scheduled, say so and I will prepare it under +RPF-WP-0035 with both custodians in one window." ops-warden deliberately did not +ask on our behalf — correctly, per their ADR-0003, since implying an owner request +from a routing errand is quiet ownership. So T04 now waits on the repository owner +deciding whether the Qonto rotation should happen, and when. Nothing indicates +compromise; this is hygiene plus the first real exercise of rotation step 4, which +is why it is a judgement call rather than an obvious yes. + +**Precedent offered, and it is the right one.** The identical two-custodian shape +was exercised 2026-09-09 for the approval clients: attended founder session, +OpenBao KV custody plus a Kubernetes-delivered consumer copy, compatible pinned +rollout, protected recovery retained, and rollback that restored configuration and +image and detached delivery before a clean resume. Their +`scripts/keycape_approval_custody.py` (activate / resume-activate / verify) is the +shape a rapp-qonto rotation should follow rather than a bare `bao kv patch` — for +exactly the provider/consumer consistency reason this task gave when it declined +to ship a wrapper around a raw KV write. They endorsed that refusal unprompted. + +**And the `warden plan` defect is fixed** (WARDEN-WP-0038, 2026-09-10). The Qonto +need now returns `founder_required` naming the write owner, and read transports +are no longer offered against a write need. ops-warden's closing line is worth +keeping: "Your caution was load-bearing and it should not have had to be." + +If the rotation is scheduled, it is also the first opportunity to close the +`real_predecessor_rotation_tested: false` gap recorded above — `verify-client` +with `-previous-secret-env` is exactly step 4, and the predecessor would finally +be genuinely distinct. + +**Decision, 2026-09-10: not yet.** The repository owner declined to schedule the +rotation now. Recorded with the reasoning so it is revisited on evidence rather +than re-litigated from scratch: nothing indicates compromise, the offer stands +open, and the estate already has one founder-attended window pending for the two +fail-closed startup changes. Rotating for hygiene alone, in a second attended +session, against a credential with no incident attached, is work the owner chose +not to spend now. + +This is a deferral, not a refusal, and the thing that should reopen it is a +change in evidence rather than the passage of time: an actual exposure or +suspected one (see KEY-WP-0011), the Qonto secret's age becoming a stated concern, +a consumer requiring proof of rotation, or a decision to prove rotation step 4 +before relying on it elsewhere. Any of those makes the answer different; a +calendar date does not. + +**Closed 2026-09-10.** The paragraph above left T04 `wait` with nothing +outstanding, which is a contradiction: `wait` means waiting or blocked, and this +was neither. Re-reading the task against its own title settles it — *admit +rotation and verify consumer handoff* — and both halves are delivered: + +- **Rotation admitted.** The semantics were reviewed and published + (`docs/native-authentication.md`), the executor and authority are named + (platform operator, attended founder session, governed + `openbao-platform-admin-login` lane, `founder_required` attended OIDC via + `role=platform-admin`), the transport is admitted, and the CCR will be prepared + on request. Step 4 shipped here as `keycape verify-client`, treating an + identical predecessor as a failure because an unchanged secret is a rotation + that did not happen. Steps 1–3 are custody's and are deliberately not + automated by this repository. +- **Consumer handoff verified.** ops-warden took option (a) for the reason we + gave rather than out of caution — the proxy yields an OpenBao token and + `keycape login` an issuer JWT, so they were never one thing to cut over + between. Lane `key-cape-oidc-login` is `owner-confirmed` with our boundary + written in verbatim, and `rapp-qonto-keycape-client` was narrowed to steps 1–2 + rather than cleared. No route was changed. + +*Executing* a rotation was never this task's deliverable, and treating the +owner's deferral as an open item would have kept a task open against work it was +never scoped to do. The deferral stands on its own above, revisited on evidence. + +Two residuals are recorded rather than carried here, because they belong to +whoever next opens a window: `real_predecessor_rotation_tested: false` and +`observed_wall_clock_expiry: false`. `verify-client -previous-secret-env` is +ready to close the first the moment a genuinely distinct predecessor exists. + +Also standing, from ops-warden and not to be lost: until WARDEN-WP-0038 lands, +**treat a `warden plan` verdict on any need containing a write, rotate or +provision act as unreliable** — it inherits the verdict of the lane that reads +the same path, and `autonomous` is documented as the signal to proceed without +the founder. That applies to this repository whenever it plans a rotation. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md new file mode 100644 index 00000000..82b952a2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md @@ -0,0 +1,29 @@ +--- +id: KEY-WP-0015 +type: workplan +title: "Reconcile repository scope with implementation and intent" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: scope-intent-assessment +created: "2026-09-05" +updated: "2026-09-05" +state_hub_workstream_id: "14a7312b-ec92-5640-8b26-cbaa4753c18e" +--- + +## Inventory implementation and document scope gaps + +```task +id: KEY-WP-0015-T01 +status: done +priority: medium +state_hub_task_id: "99be1489-d9e5-53b1-9c82-75dc961131f7" +``` + +Reviewed source revision b989de4, INTENT, SCOPE, runtime composition, authentication +commands, adapters, migration/validator code, tests, packaging and open workplans. +Updated SCOPE.md to describe implemented surfaces and their actual limits. +Recorded evidence, ten prioritized gaps and closure criteria in +history/2026-09-05-011726-scope-intent-assessment.md. INTENT and implementation are +unchanged. Findings are assessment results, not implementation completion claims. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md new file mode 100644 index 00000000..52839bd0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md @@ -0,0 +1,123 @@ +--- +id: KEY-WP-0016 +type: workplan +title: "Authorization-code protocol hardening and token verification bindings" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: authorization-code-protocol-hardening +created: "2026-09-06" +updated: "2026-09-06" +state_hub_workstream_id: "22698171-f283-59a9-9443-183faaeb6287" +--- + +Closes gap G01 of `history/2026-09-05-011726-scope-intent-assessment.md`, the +assessment's first recommended item. The browser grant validates PKCE, client id +and scopes but does not bind the redirect URI, authenticate confidential clients, +enforce grant-type eligibility, or consume the authorization code atomically. +UserInfo verification is weaker than the caller CLI's contract. + +Scope is the local protocol surface only. Upstream provider-token verification +(the Authelia adapter's deliberate no-signature-check trust assumption) is a +separate trust-contract decision and is not addressed here. + +## Consume authorization codes atomically + +```task +id: KEY-WP-0016-T01 +status: done +priority: high +state_hub_task_id: "a5171df0-4905-518d-a52f-7152b980af79" +``` + +`SessionStore.Get` reads a code and `Delete` removes it in separate lock +acquisitions, with signing in between, so two simultaneous exchanges can both +observe the same session before either deletes it. Replace the token path's +lookup with a single-operation consume that removes the session under the same +lock that reads it, so exactly one concurrent exchange can succeed. A failed +exchange must not leave a replayable code. Cover with a concurrent-reuse test +that asserts exactly one success across parallel requests. + +Added `SessionStore.Consume`, which reads and deletes under one lock, and moved +the token path onto it. Verified the defect was real before claiming the fix: +with `Get`/`Delete` restored, 9 of 16 concurrent exchanges succeeded and a +PKCE-failed exchange left the code replayable. Both now fail closed — +`TestConcurrentCodeExchangeSucceedsExactlyOnce`, `TestFailedExchangeConsumesTheCode`. + +## Bind redirect URI, grant type and confidential client authentication + +```task +id: KEY-WP-0016-T02 +status: done +priority: high +state_hub_task_id: "1d0fb108-273f-5c9a-a7c7-7640f19bad9d" +``` + +On the authorization-code path: require `redirect_uri` and compare it for exact +equality with the value stored at authorization time; reject a client whose +registration does not permit `authorization_code` (an empty `grantTypes` stays an +implicit authorization-code client, matching config validation, but a +`client_credentials`-only client must be refused); and authenticate confidential +clients with a constant-time secret comparison, accepting the same +`client_secret_basic` and form-encoded credentials as the service grant. Public +clients continue to present no secret and must not be able to supply one to +impersonate a confidential registration. Negative tests per condition. + +Implemented all three in `token.go`, with `basicClientCredentials` and +`secretsEqual` shared with the service grant (digest-first comparison, so a +wrong-length secret is indistinguishable from a wrong value). Tests: +`TestCodeExchangeRejectsRedirectURIMismatchAndOmission` (omitted, different, +traversal-shaped and trailing-slash variants), +`TestCodeExchangeRejectsServiceOnlyClient`, +`TestCodeExchangeAllowsImplicitAuthorizationCodeClient`, +`TestConfidentialCodeClientRequiresItsSecret` and +`TestPublicClientSecretIsIgnoredAndEmptyConfidentialSecretIsRejected`. The +`keycape login` CLI already sent `redirect_uri`, so no caller change was needed; +the authclient test stub was recording no redirect and is now faithful to real +`/authorize`. + +## Harden UserInfo token verification + +```task +id: KEY-WP-0016-T03 +status: done +priority: high +state_hub_task_id: "336db63a-e4fd-522d-86b6-0bf54a31af1c" +``` + +UserInfo verifies an RSA signature and expiry but ignores its own `Issuer` field +and does not check the JOSE header algorithm, the issuer claim, or the token +purpose, so an ID token or a foreign-issuer token of the right shape is accepted +where an access token is required. Enforce `alg=RS256` from the header, reject +`none` and any other algorithm, require the configured issuer, and reject tokens +that are not access tokens. Negative tests per condition. + +`validateJWT` became `validateAccessToken`, checking the JOSE header before +trusting the signature, requiring the configured issuer (an empty configured +issuer is a misconfiguration, not a skipped check), and requiring the `scope` +claim the token endpoint sets only on access tokens. Purpose is decided +verification-side deliberately: adding a `token_use` claim would change the +issued-token contract that consumers pin exactly. Tests: +`TestUserinfoRejectsUnverifiedTokenShapes` (foreign issuer, missing issuer, ID +token presented as an access token) and `TestUserinfoRejectsNonRS256Algorithms`, +which restates the header algorithm over a genuine RS256 signature so it +isolates the header check. + +## Reconcile scope and assessment records + +```task +id: KEY-WP-0016-T04 +status: done +priority: medium +state_hub_task_id: "90efa6ef-9495-5716-bee2-49d01c9ce05d" +``` + +Update `SCOPE.md`'s protocol-hardening limit and G01's closure status in the +assessment to state exactly which bindings are now enforced and which remain +open, naming the upstream-trust item as still outstanding. Do not claim complete +profile conformance: G01 closure covers these bindings, not the whole profile. + +Updated `SCOPE.md`'s protocol-hardening limit and recorded a partial-closure +status under G01 in the assessment, naming the Authelia upstream-trust item as +still open. G01 is explicitly not fully closed. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md new file mode 100644 index 00000000..00eb0397 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md @@ -0,0 +1,118 @@ +--- +id: KEY-WP-0017 +type: workplan +title: "Reconcile the canonical model and discovery metadata with the runtime" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: canonical-model-and-discovery-conformance +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "0904caad-ad8b-573f-9cdb-b6da09262a18" +--- + +Closes gap G02 of `history/2026-09-05-011726-scope-intent-assessment.md`. +`spec/canonical-model.yaml` and `src/internal/domain/model.go` both claim to be +the source of truth for the client registration shape, and they disagree: the +spec restricts grants to `authorization_code`, requires redirect URIs for every +client, and omits the service-subject, tenant, audience, lifetime, role and +MFA/handoff fields the runtime actually reads. Discovery advertises a fixed basic +scope and claim list that omits core profile claims and every configured resource +scope. + +The point of the work is the executable link, not the edit: reconciling the two +files once leaves them free to drift again the same afternoon. + +## Reconcile the client entity with the runtime model + +```task +id: KEY-WP-0017-T01 +status: done +priority: high +state_hub_task_id: "996f6895-f36b-585e-844d-3e55d02eefda" +``` + +Bring the `Client` entity in `spec/canonical-model.yaml` in line with +`domain.Client`: add `audience`, `serviceSubject`, `tenant`, `roles`, +`mfaRequired`, `registrationUrl` and `enrollmentUrl`; extend the `grantTypes` +enum to include `client_credentials`; and make `redirectUris` required only for +clients that use the authorization-code grant, matching what config validation +already enforces. Record which fields are deliberately absent from the canonical +model rather than silently omitted — `clientSecret` and `tokenLifetime` are +runtime policy, not identity data, and both are `yaml:"-"` in the Go model. + +Added all seven fields, extended the grant enum, and made `redirectUris` +optional at the schema level with the authorization-code condition stated in its +description. `tokenProfile` and `environments` were declared in the spec but read +by nothing; they are kept and marked `runtime: false` rather than deleted, since +removing them would discard intent the check can now hold honest. + +## Add an executable conformance check + +```task +id: KEY-WP-0017-T02 +status: done +priority: high +state_hub_task_id: "77df5043-2b23-56cd-947e-317f7117ac18" +``` + +Add a test that compares the canonical model's `Client` field set against +`domain.Client`'s YAML tags by reflection and fails on divergence in either +direction: a Go field with no spec entry, or a spec field the runtime does not +read. Fields intentionally excluded must be named in one list the test reads, so +excluding a field is a deliberate, reviewable act rather than an omission. This +is the generation/conformance link the assessment asks for. + +`src/internal/domain/conformance_test.go` checks `Client`, `User`, `Group` and +`Membership` by reflection over YAML tags. It earned its place on the first run +by finding drift outside the assessment's list: `User.tenant` was read by the +runtime and undeclared in the spec. `yaml:"-"` fields are excluded by +construction, which is what keeps `clientSecret` and `tokenLifetime` out without +a hand-maintained exception list. + +## Reconcile discovery metadata with the profile surface + +```task +id: KEY-WP-0017-T03 +status: done +priority: medium +state_hub_task_id: "5eb36f9b-a8e0-541e-8f81-310db49b77a0" +``` + +`claims_supported` omits `tenant`, `principal_type`, `assurance`, `scope` and +`nonce`, which the token endpoint emits on every token, and `scopes_supported` is +a fixed list that omits every configured resource scope. Advertise the core +profile claims and derive the scope list from the registered clients. Keep the +distinction the assessment asks for: an omitted core claim is a contract defect, +while optional discovery metadata is not, so document which is which rather than +treating every omission alike. + +`claims_supported` now lists the core profile claims (`tenant`, `principal_type`, +`assurance`, `scope`, `nonce`) alongside the scope-gated human claims and the +optional cached `tenant_roles`. `scopes_supported` is derived from the registered +clients — baseline OIDC scopes first, configured resource scopes sorted after, so +the document is stable regardless of map iteration order — with the baseline +still advertised when no clients are configured. +`TestDiscoveryAdvertisesEveryCoreProfileClaim` states the distinction the +assessment asked for: a missing core claim fails, optional metadata does not. + +## State a single source of truth + +```task +id: KEY-WP-0017-T04 +status: done +priority: medium +state_hub_task_id: "63290e2a-bae1-51c3-9cb9-d49994d804b6" +``` + +Two files currently claim to be authoritative. State in both that the Go model is +the runtime authority and the canonical model is the reviewed contract the +conformance check holds it to, so the next reader knows which one to edit first. +Update `SCOPE.md` and G02's status in the assessment to say what is now enforced +and what remains — this closes the client-registration and discovery drift, not +schema enforcement in general (G06 is a separate gap). + +Both files now state the split: the Go model is the runtime authority, the YAML +the reviewed contract, and the conformance test the link. SCOPE.md and G02's +status say what is enforced and that G06 remains open. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md new file mode 100644 index 00000000..325fc483 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md @@ -0,0 +1,104 @@ +--- +id: KEY-WP-0018 +type: workplan +title: "Make the LLDAP export report its own completeness" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: export-completeness-evidence +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "82ff99a1-65cc-5e1f-93fc-5bc8911ce77a" +--- + +Closes gap G05 of `history/2026-09-05-011726-scope-intent-assessment.md`. + +`src/internal/migration/lldapexport/exporter.go` discovers groups by walking each +user's memberships, so a group nobody belongs to is absent from the snapshot and +nothing says so. A `LookupGroups` failure is skipped by a `continue` under a +comment claiming it is recorded in the incompatibility report — it is not. The +run then emits `result: "success"` and the CLI reports a clean export. Group +order comes from map iteration, so two exports of an unchanged directory differ. + +A second defect falls out of reading the adapter: `LDAPAdapter.LookupGroups` +never populates `Group.Members`, and the exporter builds every membership from +that field. Against a real LLDAP directory the `memberships` block is therefore +always empty, while the fixture-based tests pass because the mock fills it in. + +The unit of work is the evidence, not the enumeration: an export that cannot +prove it saw the whole directory must say which one it is. + +## Enumerate groups independently of membership + +```task +id: KEY-WP-0018-T01 +status: done +priority: medium +state_hub_task_id: "abb11f82-06e9-58ee-8be2-6d20491e9524" +``` + +Add an optional `domain.GroupLister` capability (`ListGroups`) and implement it +on `LDAPAdapter` with a direct group-subtree search that reads `member` and +`uniqueMember`, so `Group.Members` is populated from the directory rather than +left empty. Keep it optional rather than widening `UserRepository`: the OIDC +layer has no use for it, and every existing implementation and test double would +otherwise have to grow a method it never calls. + +Added `domain.GroupLister` and `LDAPAdapter.ListGroups`, searching the group +subtree for `groupOfNames`/`groupOfUniqueNames` and reading `member` with +`uniqueMember` as the fallback attribute. Compile-time assertions on the adapter +now state both contracts it satisfies. The adapter tests assert the filter is +*not* a membership filter and that a member-less group survives the mapping, +which is the property the whole task exists for. + +## Report or fail on incomplete reads + +```task +id: KEY-WP-0018-T02 +status: done +priority: medium +state_hub_task_id: "d08f62dd-42e8-5c22-ba50-15b53f7473fd" +``` + +Record on the result which enumeration actually ran — a complete directory +enumeration or the membership-derived fallback — and treat a failed group +enumeration as fatal rather than as a silent gap. In the fallback path, record +each skipped user in the incompatibility report instead of discarding the error, +and derive memberships from the discovered user/group pairs. Emit `partial` +telemetry, and have the CLI say which enumeration produced the snapshot, so a +degraded export is legible without reading the YAML. + +`ExportResult` gained `groupEnumeration` — written unconditionally, so nobody has +to infer completeness from an absent field — and a `Complete()` predicate that +requires both a directory enumeration and an empty report. The fallback path +records its own incompleteness as a report entry before it starts, so the +snapshot says so even when every user lookup succeeds. + +The fallback also had to stop deriving memberships from `Group.Members`: the +LLDAP adapter never populated that field, so against a real directory the +`memberships` block was always empty while the fixture-backed tests passed. It +now derives each membership from the user/group pair actually observed. + +## Define ordering and test the failure modes + +```task +id: KEY-WP-0018-T03 +status: done +priority: medium +state_hub_task_id: "3cd47458-9469-5cd9-a0b1-f2894402e1f8" +``` + +Sort users, groups and memberships on stable keys so an unchanged directory +exports byte-identically. Cover the cases the assessment names and the current +suite does not: an empty group, a group enumeration failure, a per-user lookup +failure in the fallback path, and repeat-run determinism. + +Sorted users and groups by ID and memberships by group then user. Four exporter +tests and two adapter tests cover the named cases; the determinism test runs the +export three times over deliberately unsorted input and compares the full +ordering, rather than asserting on a single sorted field. + +Recorded in `SCOPE.md` and in G05's status what this does and does not establish: +completeness evidence for the user/group/membership surface, not for credential +migration, which stays under G03. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md new file mode 100644 index 00000000..dfeb9229 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md @@ -0,0 +1,155 @@ +--- +id: KEY-WP-0019 +type: workplan +title: "Verify upstream Authelia ID tokens" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: upstream-provider-token-verification +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "87ea18ea-dc60-52ce-baf4-476b46aa06f9" +--- + +Closes the remaining half of gap G01 in +`history/2026-09-05-011726-scope-intent-assessment.md`. The Authelia adapter +decodes upstream ID-token claims without verifying the signature, issuer, +audience or expiry, justified in a comment by a server-to-server TLS boundary. + +**Operator decision (2026-09-07, Bernd):** verify the token itself. The +KeyCape→Authelia hop is to be HTTPS as defence in depth, but KeyCape will not +monitor, check or gate on that — transport enforcement is friction without +protection value here, and a check that must be configured correctly to help is +itself a failure mode. Verification is chosen precisely because it does not +depend on the transport being what we believe it is. No HTTPS validation, opt-in +flag or transport telemetry is to be added. + +## Extract a shared RS256/JWKS verifier + +```task +id: KEY-WP-0019-T01 +status: done +priority: high +state_hub_task_id: "68c1b099-96b4-5065-a4be-8592438a0ca9" +``` + +`internal/authclient` already contains a strict RS256 verifier — kid required, +no `crit`, RSA-only keys of at least 2048 bits with an odd exponent. The upstream +verification needs the same rules. Duplicating security-critical verification is +how two copies drift and one silently misses a fix, so extract the reusable half +into `internal/jose`: strict JWK-set parsing and signature verification returning +claims. Claim policy stays with each caller, whose audience, nonce and issuer +rules genuinely differ. + +Added `internal/jose`: strict JWK-set parsing (RSA signing keys only, at least +2048 bits, odd exponent 3 or greater, no duplicate key ids) and RS256 signature +verification returning claims. All failures collapse to one `ErrVerification` +so a caller cannot leak which check failed back to whoever supplied the token. +`authclient` is not yet migrated onto it — see T05. + +## Verify the Authelia ID token + +```task +id: KEY-WP-0019-T02 +status: done +priority: high +state_hub_task_id: "8105030b-c91f-5a40-a221-4a38c8531e1c" +``` + +Before trusting any claim from the upstream ID token: verify the RS256 signature +against Authelia's published keys, require the issuer Authelia advertises, require +KeyCape's own client ID in the audience, and reject expired or not-yet-valid +tokens. Fail closed — an unavailable or unparseable key set denies the login +rather than falling back to unverified claims. Resolve Authelia's metadata and +key set from the server-side token base URL so a split-horizon deployment, where +the advertised public URL is not reachable from KeyCape, still verifies; allow +explicit issuer and JWKS overrides for deployments where that inference is wrong. +Refresh the key set once on an unknown key id so provider key rotation does not +require a KeyCape restart. Replace the comment claiming the TLS boundary as +justification. + +`idTokenVerifier` in the adapter resolves the issuer and JWKS path from the +provider's discovery document, rebasing the advertised `jwks_uri` path onto the +server-side token base URL so split-horizon deployments resolve, with +`issuer`/`jwksUrl` config overrides where that inference is wrong. Key sets are +cached and refetched once on an unknown key id. `parseIDTokenClaims` remains for +diagnostics but is documented as outside the authentication path. + +## Prove the rejections + +```task +id: KEY-WP-0019-T03 +status: done +priority: high +state_hub_task_id: "163f64dd-aa87-5ce3-b3be-d134fa993eb6" +``` + +Negative tests per condition: forged signature, `alg: none` and other algorithms, +unknown key id, wrong issuer, missing and wrong audience, expired token, future +`iat`/`nbf`, malformed key set, and an unreachable JWKS endpoint. A positive case +must confirm a genuine token still authenticates, and a rotation case that a new +key id is picked up after one refresh. + +Thirteen rejection cases in `verify_test.go` plus the algorithm and rotation +cases. Confirmed they catch the original defect rather than merely passing: with +the unverified parse restored, all fifteen fail. The existing adapter tests now +build genuinely signed tokens against a fixture provider, so they exercise the +real verification path instead of being routed around it. + +## Reconcile the records + +```task +id: KEY-WP-0019-T04 +status: done +priority: medium +state_hub_task_id: "8a208092-7e85-5ad9-a8c4-0dddc66aaaee" +``` + +Update `SCOPE.md` and G01's status in the assessment to state that upstream +provider tokens are now verified independently of transport, and record the +operator decision and its reasoning so the absence of transport enforcement reads +as a choice rather than an oversight. G01 closes with this; complete profile +conformance is still not claimed. + +SCOPE.md and G01's status record the verification and the operator decision, +including why no transport enforcement exists, so its absence reads as a choice. + +## Consolidate the caller verifier + +```task +id: KEY-WP-0019-T05 +status: done +priority: medium +state_hub_task_id: "0dc909fe-1772-5816-9e11-dbe7664a4e1f" +``` + +Move `internal/authclient`'s inline verification onto `internal/jose` so one +implementation serves both paths. Kept separate from T01/T02 deliberately: the +caller verifier is a tested security path, and destabilising it in the same +change that introduces upstream verification would confuse the evidence for both. + +`Client.Verify` now fetches the raw key set and delegates parsing and signature +checking to `internal/jose`, keeping its own claim policy — audience and nonce +bindings are the caller's and cannot live in a shared verifier. Its existing +tests pass unchanged, which is the point: the migration preserved behaviour. + +One deliberate strictness increase: a key set containing any malformed RSA +signing key is now refused outright, where the previous code would have used a +good key alongside a bad one. KeyCape's own `/jwks` publishes a single key, so +this affects no current deployment. + +Also added direct tests for `internal/jose` (17 cases). It is now the single +verifier behind both paths, so testing it only through its callers would leave +its edges — duplicate key ids, `crit`, even exponents, undersized moduli — to be +covered by accident. + +Follow-on evidence for the same task: "existing tests pass unchanged" shows the +move preserved behaviour, not that the behaviour is checked, so the check was +made explicit. Disabling the RSA comparison in `jose.Verify` fails both callers' +suites, which establishes the shared verifier is load-bearing on each path. +Added two caller-side cases the existing tamper case cannot reach, since it +alters signature bytes for a key that is legitimately published: a structurally +valid token signed by an unpublished key under a published kid, which fails only +if key selection is bound to the key set, and an undersized modulus in the +published set, which must deny rather than fall through to the claims. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md new file mode 100644 index 00000000..7b6f3be9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md @@ -0,0 +1,107 @@ +--- +id: KEY-WP-0020 +type: workplan +title: "Make the Keycloak transform preserve or name every policy field" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: migration-contract-preservation +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "803845de-98ad-5a5b-89ec-11bd9e0fafec" +--- + +Closes the first half of gap G03 of +`history/2026-09-05-011726-scope-intent-assessment.md` — semantic preservation. +The second half, proof against a running Keycloak, is G04 and stays open. + +`keycape-to-keycloak` calls `Transform`, which passes no clients, so the realm it +writes has an empty `clients` array and nothing says the service-identity +contract was not migrated. `TransformWithClients` exists but no caller reaches +it. Where clients are supplied, `mapClient` always enables the standard flow +regardless of grant types, and drops `Audience`, `ServiceSubject`, `Tenant`, +`Roles`, `TokenLifetime`, `MFARequired`, `SecretRef` and the handoff URLs. +`mapUser` drops the canonical ID, tenant and roles. Realm roles and client scopes +are written as empty containers with a comment saying they "can be extended". + +The failure mode is not the missing mapping — it is that a dropped field and an +inapplicable one look identical in the output. An operator diffing the realm JSON +against the KeyCape config has no way to tell what this tool decided not to carry +across. Preserve what Keycloak can express, and name the rest. + +## Give the CLI the client registrations + +```task +id: KEY-WP-0020-T01 +status: done +priority: high +state_hub_task_id: "6f6bcfd7-16c5-5311-9568-ad9aa9c3b60a" +``` + +Add a `-clients` flag reading the KeyCape config's client registrations and pass +them through `TransformWithClients`. When no client file is given, say so on +stderr and in the report rather than emitting an empty `clients` array that reads +like a realm with no clients. + +Added `-clients`, reading registrations through a new `config.Registrations()` +that converts without resolving secrets — migration tooling needs the contract, +never the material, so `ClientSecret` is left empty by construction rather than +by remembering not to use it. An unparseable `tokenLifetime` is an error there, +since silently dropping a per-client lifetime is the defect class this closes. +Run against `config/dev-config.yaml`, all five registrations now reach the realm. + +## Preserve what Keycloak can express + +```task +id: KEY-WP-0020-T02 +status: done +priority: high +state_hub_task_id: "88c0126c-fd73-5178-af1f-a018708ff8fb" +``` + +Derive `standardFlowEnabled` and `serviceAccountsEnabled` from the declared grant +types instead of hardcoding the standard flow on. Carry the audience, tenant, +service subject, roles and principal type as protocol mappers, per-client +lifetime as the Keycloak client attribute that expresses it, and the handoff URLs +and secret reference as attributes — the reference, never a secret value. Derive +realm roles and client scopes from the users and clients actually present rather +than emitting empty containers. Carry the user's canonical ID, tenant and roles. + +Flows now follow the grants: the old mapping set `standardFlowEnabled` on every +client, so migrating a `client_credentials` service registration silently gave it +the browser flow. Audience, tenant, service subject and roles ride as protocol +mappers, since Keycloak has no native concept for the profile's claims and would +otherwise issue tokens the profile rejects. Lifetime, handoff URLs and the secret +*reference* are attributes; a test marshals the realm and fails if a resolved +secret value appears, because a realm import file is not a custody boundary. +Realm roles and client scopes are derived and sorted. + +## Name everything not preserved + +```task +id: KEY-WP-0020-T03 +status: done +priority: high +state_hub_task_id: "5b8127b2-c01c-5d3c-afbf-adbb368941e6" +``` + +Report each field the transform cannot carry, at the point it is dropped, so the +output distinguishes a deliberate omission from a gap: passwords and MFA +credentials, MFA policy enforcement, and subject continuity — LLDAP's canonical +ID is a DN, and Keycloak will mint its own `sub`, so tokens after a migration +will not carry the same subject unless something downstream maps it. Fail the +realm-level validation when a supplied client's contract is not fully +represented. Test that each unpreserved field appears in the report. + +`UnpreservedReport()` is separate from `ValidationReport()`. Folding them +together was the first attempt and it broke an existing test asserting a clean +export reports nothing — correctly so: consistency with the snapshot and +completeness of the migration are different questions, and one list cannot answer +both. Keeping them apart preserves the existing contract and makes the +distinction the point rather than a side effect. + +Six tests cover the flows, the mappers and lifetime, the secret-value exclusion, +the derived roles and scopes, every named limit, and the separation of the two +reports. Mutation-checked: restoring the hardcoded `standardFlowEnabled` fails +the flow test. An incomplete transform now emits `partial` telemetry. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md new file mode 100644 index 00000000..1f6efeb3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md @@ -0,0 +1,101 @@ +--- +id: KEY-WP-0021 +type: workplan +title: "Make snapshot attribute validation enforce a real rule" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: snapshot-attribute-validation +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "733ad1a5-c4d8-5c2f-8608-c51bf1d5b17e" +--- + +Closes gap G06 of `history/2026-09-05-011726-scope-intent-assessment.md`: +`checkNoUnknownAttributes` is named for a rule it does not implement. It rejects +blank keys, which is not an allow-list, so a snapshot carrying arbitrary or +malformed LDAP attribute names passes a check whose name says otherwise. A rule +that passes for the wrong reason is worse than an absent one, because a report +listing `no_unknown_attributes: passed` is read as evidence. + +One part of the assessment's description needs correcting rather than +implementing: it says group membership validation checks nonempty member IDs +rather than complete referenced identity existence. `checkValidGroupMemberships` +does only check emptiness, but the semantic rule `checkReferencedUsersExist` +already resolves every member against the user set and fails on an unknown one. +The reference constraint exists; it lives in a different rule than the one the +assessment looked at. + +## Enforce real attribute constraints + +```task +id: KEY-WP-0021-T01 +status: done +priority: medium +state_hub_task_id: "e2921983-853c-501b-8dfc-158751dea093" +``` + +`ldapAttributes` is defined by the canonical model as raw attributes *not covered +by* the model, so an allow-list of permitted names cannot be derived from the +schema — the field exists precisely to carry what the schema does not name. Two +constraints are derivable and worth enforcing: + +- the attribute name must be a syntactically valid LDAP attribute descriptor + (RFC 4512: a letter followed by letters, digits or hyphens, or a numeric OID), + since a name that cannot be written to a directory cannot round-trip; and +- it must not shadow a canonical field's documented LDAP mapping (`uid`, `cn`, + `mail`), because two sources for one value is how an export and a directory + come to disagree, and the model already owns those. + +Attribute descriptors are case-insensitive in LDAP, so keys differing only by +case are a collision, not two attributes. Keep the blank-key check. Rename the +rule to describe what it actually enforces rather than leaving a name that +overpromises. + +Implemented as `raw_attributes_well_formed`, replacing `no_unknown_attributes` +in both `validator.go` and `spec/ldap-schema.yaml`, with the schema entry stating +plainly that this is not an allow-list and why one cannot be derived. + +## Prove each rule with an invalid snapshot + +```task +id: KEY-WP-0021-T02 +status: done +priority: medium +state_hub_task_id: "de4caf0a-39c6-5148-a986-db18a1308505" +``` + +The assessment asks for invalid-snapshot cases proving each stated rule. Add one +per constraint — malformed descriptor, shadowed canonical mapping, case-only +duplicate, blank key — plus passing cases for legitimate raw attributes and a +valid numeric OID, so the rule is shown to be selective rather than merely +strict. + +Eleven malformed-descriptor cases, six canonical-mapping cases across casings, a +case-only duplicate, a message-content case, and five accepted-attribute cases. +Confirmed they test the rule rather than merely passing: making the check return +early fails four of the five tests. + +## Reconcile the records + +```task +id: KEY-WP-0021-T03 +status: done +priority: medium +state_hub_task_id: "8a48c6cc-1e7e-5fd4-abfa-2b893c3d400d" +``` + +Update `SCOPE.md` and G06's status. State what the rule now enforces and, just as +importantly, what it still does not: this is not schema-derived validation of +every attribute against a directory schema, and the canonical model cannot +provide one for a field defined as everything the model does not cover. Correct +the assessment's reference-constraint premise rather than restating it. + +`history/…-scope-intent-assessment.md` was being edited by a concurrent session +when this workplan was written; make this edit only against a clean tree. + +Done against a clean tree after the other session committed. G06's status records +both corrections: no allow-list is derivable for a field defined as what the +model does not cover, and the reference constraint was already implemented in +`checkReferencedUsersExist` rather than missing. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md new file mode 100644 index 00000000..7c69629e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md @@ -0,0 +1,136 @@ +--- +id: KEY-WP-0022 +type: workplan +title: "Make the replacement harness runnable and target conformance externally" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: replacement-harness-and-external-conformance +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "94d74b17-1b2f-57dd-b6c3-50f10a7dccd9" +--- + +Closes the runnable half of gap G04 of +`history/2026-09-05-011726-scope-intent-assessment.md`. The Scenario B and C +scripts cannot execute: they reference compose files that do not exist, call +binaries at `src/bin/` where the Makefile builds to root `bin/`, pass `--base-dn` +to `lldap-to-ldap` whose flag is `--basedn`, and invoke a hardcoded workstation Go +path from outside the Go module. + +The deeper problem is that repairing the shell would not prove anything. Both +scripts set `KEYCAPE_TEST_ISSUER`, but the profile suite builds its own `httptest` +server and never reads it, so the tests pass identically whether or not the +external provider is running. A harness that cannot fail for the reason it exists +is worse than a missing one. + +## Repair the harness prerequisites + +```task +id: KEY-WP-0022-T01 +status: done +priority: high +state_hub_task_id: "1d552328-e75b-57fd-83bc-e469ae46b397" +``` + +Fix the binary paths, the `--basedn` flag, and the Go invocation so the scripts +run from a clean checkout with `make build`: use the module directory rather than +a workstation-specific Go path, and fail with a clear message when a prerequisite +binary or a required environment value is absent instead of part-way through. + +Scenario B rewritten: root `bin/`, `go -C src` so the module root is right, +prerequisite checks before anything starts, and a cleanup trap so a failure does +not leave a stack running. Scenario C repaired in place — `bin/` paths, `--basedn` +for `lldap-to-ldap` (`lldap-export` correctly takes `--base-dn`; the two flags +genuinely differ), and the same module-relative test invocation. + +## Add the missing stacks + +```task +id: KEY-WP-0022-T02 +status: done +priority: high +state_hub_task_id: "06f34e8b-f3d9-53ed-95c5-02e5063774a0" +``` + +Provide `docker-compose.scenario-b.yml` (Keycloak over the migrated realm) and +`docker-compose.scenario-c.yml` (OpenLDAP seeded from generated LDIF, plus +Keycloak). Validate them with `docker compose config` rather than assuming they +parse. Whether a full scenario passes end to end is a separate claim from whether +the stack definition is valid, and only the second is established here. + +Both files added and validated with `docker compose config`. Scenario B was also +started for real; Scenario C's stack has not been run. + +## Target conformance at an external issuer + +```task +id: KEY-WP-0022-T03 +status: done +priority: high +state_hub_task_id: "d837730d-e9d0-5adb-9dda-ffc47661fca3" +``` + +Add a suite that reads `KEYCAPE_TEST_ISSUER` and exercises that issuer over HTTP: +discovery metadata, the JWKS parsing under the same strict rules the runtime +applies, and — when client credentials are supplied — a real token exchange whose +claims are verified against the published keys. It must skip cleanly when the +variable is unset so ordinary `make test` is unaffected, and it must fail when +pointed at an issuer that does not meet the profile, which is the property the +current harness lacks. + +`src/tests/conformance` checks that the issuer advertises itself, the profile +authorization surface (code, authorization_code, S256, RS256), that published +keys parse under `internal/jose` — the same rules the runtime applies, so a +provider publishing keys KeyCape would refuse fails here rather than at first +login — that excluded grants are absent, and, with credentials, that an issued +token verifies against the published keys and carries a matching issuer. + +## Prove the suite against a real provider + +```task +id: KEY-WP-0022-T04 +status: done +priority: high +state_hub_task_id: "cb4d8af7-76a4-50b8-bee5-eb1c52c2f894" +``` + +Run the new suite against an actual Keycloak instance, not only against KeyCape, +since the point of the scenario is a replacement provider. Record what genuinely +passed and what remains unproven. Do not describe a partially exercised scenario +as a demonstrated live swap. + +Run against Keycloak 26.0. Discovery, authorization surface and published-key +checks passed. A `client_credentials` exchange against a service client created +through the admin API returned a token that verified against Keycloak's JWKS via +`internal/jose`, with a matching issuer claim — the end-to-end property a relying +party depends on, proved against a replacement provider rather than against +KeyCape's own handlers. + +The suite also failed, correctly. Stock Keycloak advertises `implicit` and +`password` in its discovery document. In Keycloak these are server capabilities, +not per-client toggles, so no realm the transformer emits can remove them: a +migrated Keycloak presents a wider grant surface than KeyCape. Scenario B +therefore reports a real failure today, which is the harness working. KeyCape +itself was not targeted in this run — the dev stack needs key material absent +from the checkout — so the suite is proved against the replacement provider, not +yet against both sides of the swap. + +## Reconcile the records + +```task +id: KEY-WP-0022-T05 +status: done +priority: medium +state_hub_task_id: "2cb63801-8182-5c71-ab8d-9a1c377f418c" +``` + +Update `SCOPE.md` and G04's status with what the harness now does and what +replacement claims remain unproven — in particular that directory migration, MFA +and unchanged relying-party behaviour against a live Keycloak are not established +by this work. G04 does not fully close here. + +SCOPE.md and G04's status record both the working harness and the grant-surface +finding, which substantiates with evidence what SCOPE previously asserted without +it. Scenario C has still never been run end to end. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md new file mode 100644 index 00000000..3e7c83cb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md @@ -0,0 +1,136 @@ +--- +id: KEY-WP-0023 +type: workplan +title: "Prove the migration against live directories and fix what that surfaced" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: live-migration-proof +created: "2026-09-08" +updated: "2026-09-08" +state_hub_workstream_id: "9f19bfa0-78d1-5b5c-9b64-e251fa4838d6" +--- + +Finishes the unproven half of gap G04. KEY-WP-0022 made the harness runnable and +targeted conformance at a live issuer; what remained was directory migration +against a real directory, both sides of the swap measured, and the explicit +limits recorded. + +Running it found four defects that no amount of reading would have. That is the +result worth keeping: every one of them passed the full unit suite. + +## Run the export against a live LLDAP + +```task +id: KEY-WP-0023-T01 +status: done +priority: high +state_hub_task_id: "b7b2ea88-8457-5fae-a500-a2d0f75754de" +``` + +**Defect: every LLDAP user search pointed at a branch that does not exist.** +`Config.userOU()` defaulted to `ou=users`; LLDAP stores users under `ou=people`, +and nothing in this repository set `UserOU` to correct it. `LookupUser`, +`ListUsers` and `ValidatePassword` all derive their search base from it, so all +three silently found nothing against a stock LLDAP — human login included, not +just the export. + +Symptom that exposed it: exporting from a live LLDAP returned **zero users while +still emitting a membership referencing `uid=admin`**, a snapshot the repository's +own validator rejects with `referenced_users_exist`. Default corrected to +`ou=people` and pinned by a test; the re-run exports one user and validates +clean. + +## Correct the rule I broke in KEY-WP-0021 + +```task +id: KEY-WP-0023-T02 +status: done +priority: high +state_hub_task_id: "2f988626-ed08-52ce-919a-cd45d48e712d" +``` + +**Defect of my own making.** The LLDAP adapter records `_validation_warning` +inside `LDAPAttributes`. The `raw_attributes_well_formed` rule added in +KEY-WP-0021 rejects underscores as invalid attribute descriptors, so the +exporter's own annotation made its own output fail validation — a false positive +shipped one workplan earlier and caught only by validating a real export. + +Keys prefixed with `_` are now recognised as tooling annotations and skipped, +documented as such. A test proves the exemption does not weaken the rule: a +malformed real attribute alongside an annotation still fails. + +## Prove the LDIF loads into a real OpenLDAP + +```task +id: KEY-WP-0023-T03 +status: done +priority: high +state_hub_task_id: "0f503342-0ca8-55ad-b7e4-e2316d4d7bad" +``` + +**Defect: migrated group memberships were dangling.** `resolveMemberDN` returned +a member that already looked like a DN unchanged, so members kept their *source* +DNs (`uid=admin,ou=people,…`) while `writeUser` wrote entries to the *target* +branch (`ou=users`). Groups named entries the migrated directory does not +contain. Only the naming value is carried across now; branch and naming +attribute come from the target. + +**Defect: empty groups could not be loaded at all.** `groupOfNames` makes +`member` a MUST, so OpenLDAP rejected every memberless group with an object class +violation and aborted the load. Since KEY-WP-0018 the export deliberately +preserves empty groups, so this was reachable by any real migration. Empty groups +now reference a placeholder entry the same LDIF creates — an `organizationalRole`, +not a person, so it cannot be mistaken for a migrated user — emitted only when +some group needs it. + +**Defect: the compose file could not start.** `bitnami/openldap:2.6` does not +exist in the registry. It passed `docker compose config` in KEY-WP-0022, which is +exactly the gap between a file that parses and a stack that runs. Pinned to +`osixia/openldap:1.5.0`, the image the scenario was actually proved against. + +Proof: LLDAP → export → validate → LDIF → `ldapadd` into OpenLDAP 1.5.0, all +seven entries added with no errors, and every `member` value in the loaded +directory resolves to an entry that exists. + +## Measure both sides of the swap + +```task +id: KEY-WP-0023-T04 +status: done +priority: high +state_hub_task_id: "8a7fe95e-d463-5e74-81fa-5fa60a879774" +``` + +KeyCape was run locally against a generated key and targeted with the +conformance suite: all five checks pass, including the excluded-grant check +Keycloak fails. The Keycloak side was exercised twice — the stock realm in +KEY-WP-0022, and here the *migrated* realm produced from the live export, which +imported cleanly and served discovery. The migrated user arrived carrying +`keycape.canonicalId`. + +The divergence is now measured rather than asserted: KeyCape passes 5/5, a +migrated Keycloak passes 4/5 and fails on advertising the excluded `implicit` and +`password` grants. + +## Record what is proved and what is not + +```task +id: KEY-WP-0023-T05 +status: done +priority: medium +state_hub_task_id: "acef6ceb-6dea-56e1-94bc-e5d589ef3b6f" +``` + +G04 is substantially closed: stacks are reproducible and were run, conformance is +externally targeted and was run against three issuers, and directory migration is +proved end to end into a real directory. + +Not closed, and stated as such: no browser login was performed against the +migrated realm, so unchanged relying-party behaviour is not established, and MFA +was not exercised. Credential and MFA migration is a **scope exclusion** rather +than a test gap — SCOPE says it is not supplied, so no harness can demonstrate +it. `keycape-to-keycloak` already reports that subject continuity is not +preserved: the canonical ID survives as an attribute while Keycloak mints its own +`sub`, so relying parties keyed on `sub` will not recognise migrated users. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md new file mode 100644 index 00000000..baa0bbd1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md @@ -0,0 +1,62 @@ +--- +id: KEY-WP-0024 +type: workplan +title: "Wire tenant_roles as explicit opt-in configuration" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: tenant-roles-opt-in-wiring +created: "2026-09-08" +updated: "2026-09-08" +state_hub_workstream_id: "d75f52f2-df0d-5a56-8394-892feeec4b4d" +--- + +Closes gap G07. The tenant-engine client and `TokenHandler.TenantEngine` were +implemented and tested, but `main.go` supplied no client and exposed no +configuration, so the stock executable left the field nil and never emitted +`tenant_roles`. The capability existed at library level only. + +Wired rather than documented away as library-only: the claim is cheap, the +adapter already fails open, and leaving a tested capability unreachable from the +binary invites the same gap being rediscovered later. + +## Add opt-in configuration and wire the client + +```task +id: KEY-WP-0024-T01 +status: done +priority: medium +state_hub_task_id: "0e53485b-7e94-5dd8-8e02-1d18f5413a78" +``` + +Added a `tenantEngine` config block with `baseURL` and an optional `timeout`. +An empty `baseURL` disables the claim, which keeps the stock server's behaviour +exactly as it was — enabling it is a deliberate act. + +Validation treats a configured source as one that must work: the URL must be +http/https with a host, and the timeout must parse and fall in (0, 10s], since it +sits on the synchronous token-issuance path. A `timeout` set without a `baseURL` +is rejected rather than ignored — it means someone expected the claim to be on +and it silently would not have been. + +## Verify it in the built executable + +```task +id: KEY-WP-0024-T02 +status: done +priority: medium +state_hub_task_id: "c4279ce0-f6f8-5612-a48a-c1d2fd3da3b6" +``` + +G07's closure criterion is the built executable, not the wiring, so all three +behaviours were checked by running `bin/keycape` and inspecting real tokens +rather than by reading the code: + +- configured against a stub tenant-engine, a `client_credentials` token carries + `tenant_roles: [capability:approve, capability:observe]`; +- with no `tenantEngine` block, the claim is absent; +- configured but with the source down, issuance still succeeds and the claim is + absent — the fail-open behaviour the adapter documents, confirmed end to end. + +Nine validation cases cover the accepted and rejected configurations. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md new file mode 100644 index 00000000..a53e6467 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md @@ -0,0 +1,118 @@ +--- +id: KEY-WP-0025 +type: workplan +title: "Give the runtime real readiness, graceful shutdown and stated operational limits" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: runtime-lifecycle-and-readiness +created: "2026-09-08" +updated: "2026-09-08" +state_hub_workstream_id: "70f74bbd-6748-5bbe-b261-a310b6f24e0c" +--- + +Closes gap G08. `/healthz` returned a constant without probing anything, the +server called `ListenAndServe` with no signal handling, and the operational +limits of in-memory state, startup-loaded keys and local-only logout were spread +across code comments rather than stated anywhere an operator would look. + +G08 explicitly does not require shared storage or refresh tokens. The work is +therefore to make readiness real, make restarts survivable for in-flight +requests, and write the limits down honestly. + +## Separate readiness from liveness + +```task +id: KEY-WP-0025-T01 +status: done +priority: medium +state_hub_task_id: "0e0d3349-efaf-546c-a58d-2a9b736278a9" +``` + +Added `internal/server/readiness` and a `/readyz` endpoint probing LLDAP, +Authelia and privacyIDEA. `/healthz` stays liveness and deliberately probes +nothing: wiring liveness to dependency health means an orchestrator restarts +KeyCape when a dependency blinks, and since a restart also discards every +in-flight login, the reaction is worse than the condition. + +Design decisions worth keeping: + +- **LLDAP is probed with a bind, not a dial.** A rotated or revoked service + password leaves the port open and every lookup failing — precisely the state + readiness exists to catch, and precisely what a TCP dial would miss. +- **The body names the failing check but never the reason.** The endpoint is + unauthenticated, and upstream error text carries hostnames and sometimes + credentials-in-URLs. Detail goes to the log. A test asserts a probe error + containing a credentialed URL does not reach the response. +- **Results are cached for 2s.** Not an optimisation: without it, anyone able to + reach an unauthenticated endpoint could drive one upstream request per + dependency per call. +- **Probes run concurrently, each bounded at 3s**, so readiness costs the slowest + probe rather than their sum, and a hung dependency makes the endpoint answer + rather than hang alongside it. + +## Shut down without dropping requests + +```task +id: KEY-WP-0025-T02 +status: done +priority: medium +state_hub_task_id: "0bea0780-523a-587e-a157-d429df15307b" +``` + +`SIGTERM`/`SIGINT` now drain in-flight requests for up to 15 seconds. The grace +period sits under the 30s read/write timeouts so a stuck request cannot outlive +the window an orchestrator allows before `SIGKILL`. + +In-memory login state is deliberately not preserved: a browser mid-login must +start again after a restart. That is a property of the single-replica topology, +documented rather than papered over. + +## State the operational limits + +```task +id: KEY-WP-0025-T03 +status: done +priority: medium +state_hub_task_id: "97eba4bb-f8a0-5941-bdc8-6d59b0fa2942" +``` + +`docs/operations.md` states the supported topology — exactly one replica, because +authorization codes, login sessions and handoffs are process-local and no sticky +configuration makes a second replica safe — and the consequences: restarts drop +in-flight logins while issued tokens keep working, and rolling deploys have a +window where new logins fail. + +It also records three limits that are easy to get wrong in operation: + +- the key ID is the constant `key-1`, so rotating the key without changing it can + leave consumers caching JWKS by `kid` rejecting freshly issued tokens; +- removing a client registration does not revoke its issued tokens, since there + is no revocation or introspection endpoint — they stay valid until expiry; +- `/logout` clears the local session only, not the Authelia session or any token. + +No throughput or resource figures are given: nothing here benchmarks KeyCape, so +any number would be invented. + +## Verify in the running server + +```task +id: KEY-WP-0025-T04 +status: done +priority: medium +state_hub_task_id: "bc64fdc2-0a31-5dab-afd1-5e7b6d11d2b3" +``` + +Checked against the built executable rather than the handlers, following the +lesson from KEY-WP-0023: + +- with every dependency down, `/readyz` returns 503 naming all three as failed + while `/healthz` still returns 200 — the split doing its job; +- starting LLDAP flips its check to `ok` while the others stay failed, so the + probes are measuring what they claim; +- 40 requests issued across a `SIGTERM` all returned 200, and the log shows the + signal handled and shutdown completed. + +Seven unit tests cover the reporter, including the disclosure, caching, timeout +and concurrency properties above. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md new file mode 100644 index 00000000..1f96a2ac --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md @@ -0,0 +1,123 @@ +--- +id: KEY-WP-0026 +type: workplan +title: "Reconcile packaging, bootstrap and migration credential handling" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: packaging-bootstrap-and-credential-handling +created: "2026-09-08" +updated: "2026-09-08" +state_hub_workstream_id: "d2b04d59-41b1-531b-8cab-a980328611b4" +--- + +Closes gap G09. Five separate defects, only loosely related: the dev stack could +not start from a clean checkout, the image shipped one binary of five, the +publish workflow named a different registry than the one the cluster pulls from, +the exporter took a password on argv, and every migration artifact was written +world-readable. + +## Take the bind password off argv + +```task +id: KEY-WP-0026-T01 +status: done +priority: medium +state_hub_task_id: "510a4603-337f-5ceb-b6b7-b9126d8605f0" +``` + +`lldap-export` took `--bind-pw`, so the service account password was visible to +any local user through `ps`, and captured by shell history and process +accounting. It now prefers `KEYCAPE_LLDAP_BIND_PW`, accepts `--bind-pw-file`, and +keeps `--bind-pw` working with a warning — deprecated rather than removed, +because existing runbooks use it and silently breaking them would be worse than +the exposure for one more cycle. + +Conflicting sources are rejected rather than silently ranked: with two supplied, +an operator cannot tell which bind was attempted. A password file has its +trailing newline trimmed, since editors and heredocs add one and binding with it +fails confusingly. Both migration scripts now pass the password by environment. + +## Stop writing migration artifacts world-readable + +```task +id: KEY-WP-0026-T02 +status: done +priority: medium +state_hub_task_id: "679e21a1-e37b-574b-bc25-b9fd46bdfd16" +``` + +The canonical export, the generated LDIF and the Keycloak realm were all written +`0644`. None carries credential material, but the snapshot is a directory dump — +every username, display name, email and group membership in the estate — and it +tended to land in `/tmp`. All three are `0600` now. + +## Ship every binary in the image + +```task +id: KEY-WP-0026-T03 +status: done +priority: medium +state_hub_task_id: "5252a13c-cd5d-5686-a7f5-04587d7d9b1d" +``` + +The image packaged `keycape` alone, so the validator and the three migration +binaries needed a Go toolchain on the host — which defeats shipping an image for +the cutover work they exist to support. All five are built and copied; the issuer +remains the entrypoint and the rest are reachable by overriding it. Verified by +building the image and running each binary inside it. + +## Reconcile the publish target + +```task +id: KEY-WP-0026-T04 +status: done +priority: medium +state_hub_task_id: "45b8c386-41bd-514c-9247-5eba9663d8fb" +``` + +The workflow published to `92.205.130.254:32166/coulomb/key-cape` while the +cluster runs `forgejo.coulomb.social/coulomb/key-cape` (KEY-WP-0013). Whether +those are one registry behind two names or two registries is not determinable +from this repository, which is exactly the problem: a push could not be assumed +to have deployed the current source. Now defaults to the recorded Forgejo name +and stays overridable through a repository variable. + +**Unverified, and needs an operator's eye:** this repository cannot test that the +runner resolves that hostname or that `REGISTRY_USER`/`REGISTRY_TOKEN` are valid +for it. If the next publish fails, set the `REGISTRY` repository variable back to +the address. + +## Make the dev stack bootstrappable + +```task +id: KEY-WP-0026-T05 +status: done +priority: medium +state_hub_task_id: "a750ac2f-cad2-5e7f-a505-2ca6b94f1a5a" +``` + +`docker-compose.dev.yml` mounts `config/dev-key.pem` and `config/authelia/`, +neither in the checkout — correctly, since one is a private key and the other +carries a password hash. `scripts/bootstrap-dev.sh` generates both locally: an +RSA key created under `umask 077` rather than chmodded afterwards, so it is never +briefly world-readable, plus an Authelia configuration, its OIDC signing key and +a user fixture. Everything it writes is git-ignored, and re-running it leaves +existing material alone. + +The fixture password is published in the script deliberately: a reader can see +exactly what it unlocks, which is nothing deployed. + +## Reconcile the records + +```task +id: KEY-WP-0026-T06 +status: done +priority: medium +state_hub_task_id: "cc7c86ae-9c78-5422-8f3c-19441ceeb513" +``` + +`SCOPE.md` and G09's status record what is fixed and what is not: the registry +change is unverified from here, and a documented external bootstrap remains the +route for anything beyond development. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md new file mode 100644 index 00000000..57e01b2d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md @@ -0,0 +1,130 @@ +--- +id: KEY-WP-0027 +type: workplan +title: "Establish the live state and the rollout gap for G10" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: rollout-readiness-and-live-state +created: "2026-09-08" +updated: "2026-09-08" +state_hub_workstream_id: "2ab60558-ee14-542c-9f19-bb487de8e668" +--- + +G10 cannot be closed from this repository: it waits on named custody and platform +owners. What was doable was to stop guessing about the live state, verify the +handoffs actually went out, and find out whether main is safe to roll out. The +last question turned up a defect in this repository's own recent work. + +## Verify the handoff receipts independently + +```task +id: KEY-WP-0027-T01 +status: done +priority: medium +state_hub_task_id: "00de2bad-04da-5b58-8cde-aaa144272541" +``` + +KEY-WP-0009 was reopened once because delivery was claimed without receipts, so +the claim that it has now been sent deserved the same scrutiny rather than being +taken on trust. `GET /messages/?from_agent=key-cape` shows all seven present and +dated 2026-09-08T08:21Z: the four KEY-WP-0009 handoffs (netkingdom, +secrets-engine, ops-warden, railiance-platform) and the three admission requests +(railiance-platform custody, approval-engine human callback, ops-warden token +split). No replies had arrived at the time of writing, which is expected hours +after sending and is not itself a blocker to record. + +## Record the live state as observed, not as remembered + +```task +id: KEY-WP-0027-T02 +status: done +priority: medium +state_hub_task_id: "2d513229-1dbd-500e-9100-c0173be7b4de" +``` + +Read-only cluster inspection, `sso` namespace, 2026-09-08: + +- image `forgejo.coulomb.social/coulomb/key-cape:main-153258b`, predating every + change from KEY-WP-0016 onward; +- environment carries `KEYCAPE_RAPP_QONTO_CLIENT_SECRET` only, so the two + approval clients are still not materialized — KEY-WP-0013-T02 remains correctly + blocked; +- four registered clients: `demo-app`, `netkingdom-bootstrap-console` and + `openbao-admin` (public, authorization_code) and `rapp-qonto-client` + (confidential, client_credentials); +- no `tenantEngine` block, so KEY-WP-0024 is inert on rollout. + +**Correction to an earlier claim of mine.** I reported the `ou=people` fix +(KEY-WP-0023) as a production-relevant correction to human login. The deployed +config sets `userOU: "ou=people"` explicitly, so production was never affected. +The fix matters for anyone relying on the default — the dev stack, the scenario +scripts, a fresh deployment — not for this one. + +## Find out whether main is safe to roll out + +```task +id: KEY-WP-0027-T03 +status: done +priority: high +state_hub_task_id: "23c8d04b-c2b7-5347-80a0-6388c1c41c53" +``` + +**Defect found in KEY-WP-0019, before it shipped.** Upstream verification +discovers the expected issuer server-side from `authelia.tokenBaseURL`. The +deployed Authelia derives its advertised issuer from the request Host, confirmed +directly: + +| Host presented | issuer advertised | +| --- | --- | +| `authelia.sso.svc.cluster.local:9091` | `http://authelia.sso.svc.cluster.local:9091` | +| `auth.coulomb.social` | `http://auth.coulomb.social` | + +KeyCape fetches over the in-cluster address, so it would expect the first value +while tokens minted for the browser-facing host carry the second. Verification +fails closed, so if they differ **every human login fails after rollout**, and it +presents as a broken login rather than a configuration error. + +Which value the ID token actually carries could not be settled from here without +performing a real login against production Authelia, which was not done. The risk +is therefore recorded as a rollout precondition, not as a confirmed breakage. + +Two mitigations, both in this repository: + +- `docs/operations.md` documents pinning `authelia.issuer` and `authelia.jwksUrl` + explicitly, with the one-line `curl` that reveals the value the provider will + advertise for a given Host. The overrides already existed; nothing said when + they were needed. +- The failure is now diagnosable. The authentication failure event carries a + specific reason — `id_token_issuer_mismatch`, distinct from + `id_token_signature`, `provider_keys_unavailable` and the rest — so an operator + can tell a misconfiguration from an attack. Five cases cover the mapping. The + browser still learns nothing. + +## Record what each owner owes + +```task +id: KEY-WP-0027-T04 +status: done +priority: medium +state_hub_task_id: "7ba84b61-0458-5820-8ded-c6f59d820303" +``` + +G10's status now names the live state, the outstanding requests with their +receipt ids, and the issuer precondition that must be checked before main is +deployed. The precondition was also sent to railiance-platform as the deployment +owner (message c8b1ad10), with the exact `curl` to settle it, the config keys to +pin, and the `error_type` to look for if it bites — plus a note that the two +approval clients remain unprovisioned, confirmed by inspection rather than +asserted. G10 stays open: nothing here admits custody, registers the human +callback, or reconciles the OpenBao-token boundary, and no repo-local change can. + + +2026-09-08 live return: the actual signed-token uncertainty in T03 is now +resolved by KEY-WP-0013-T02's admitted probe. The issuer is exactly +`https://auth.coulomb.social`; signature/audience/time/nonce checks passed and +the Job exited 0. The temporary route, policies, service and Job/Pod were removed. +`docs/evidence/2026-09-08-upstream-issuer-live-proof.json` is the source receipt. +The configuration owner still ensures the exact pin before main rollout; no +normal config or Deployment changed during this proof. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md new file mode 100644 index 00000000..46b0672f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md @@ -0,0 +1,89 @@ +--- +id: KEY-WP-0028 +type: workplan +title: "Reject service-identity fields that a browser client silently ignores" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: browser-client-field-validation +created: "2026-09-09" +updated: "2026-09-09" +state_hub_workstream_id: "6d0cb4fd-17da-5466-8011-5ade1ac46171" +--- + +`serviceSubject` and `roles` are read only on the `client_credentials` path. On a +browser client they are accepted at startup and then ignored: the subject and +roles come from the directory user. A registration that looks effective and is +not fails later, somewhere else, as a downstream rejection rather than as a +registration defect. + +`tokenLifetime` was already rejected this way for non-service clients, so the +rule exists — it was just incomplete. + +## Reject the ignored fields + +```task +id: KEY-WP-0028-T01 +status: done +priority: medium +state_hub_task_id: "e4bce8c7-0e60-55e8-ad32-a3a0c4e46044" +``` + +Config validation now rejects `serviceSubject` and `roles` on any client that +does not use `client_credentials`, naming where the value actually comes from. +Nothing in `config/dev-config.yaml`, the example fixture or the live deployment +sets either field on a browser client, so this breaks no existing configuration — +checked against all three rather than assumed. + +Re-verified against `sso/keycape-config` on 2026-09-09, after a peer raised that +this validation fails closed at startup and a rollout is imminent, so a stray +field would surface as KeyCape failing to boot. The deployed config now holds six +clients: `demo-app`, `netkingdom-bootstrap-console` and `openbao-admin` are +browser clients carrying neither field, while `rapp-qonto-client`, +`secrets-engine-approval` and `approval-engine-operator` carry both legitimately +on the `client_credentials` path. The rule rejects none of them. Kept as errors +rather than downgraded to warnings on that evidence: a warning for a field that +does nothing is the state this change exists to end. + +**`tenant` is deliberately excluded from the rule**, and a test pins that. A +browser client may declare a tenant; `humanTenant` resolves it against the +directory and refuses issuance when the two disagree (KEY-WP-0013-T05). An +earlier version of this change rejected `tenant` too, which would have made that +feature unusable. + +## Correction + +```task +id: KEY-WP-0028-T02 +status: done +priority: medium +state_hub_task_id: "5ab951f8-2f9c-522c-b6ea-7f6d572cd618" +``` + +This work started from KEY-WP-0013-T05's blocker paragraph, which described a +human token as unable to carry `tenant:platform`. That was true when written and +had already been fixed in a concurrent session by the time this task began; the +paragraph was read as current state rather than as a dated record. + +Two consequences, both corrected within the session: + +- the `tenant` rejection was removed before it was committed, and a test now + asserts a browser client carrying `tenant: tenant:platform` validates cleanly; +- a hub decision (`0145ab57`) recording the tenant question as open and + unimplemented was withdrawn with a rationale pointing at the implementation. + Nothing was built on it. + +A third instance followed the same day, outside this task: a message to +railiance-platform (d361bf28) listed the upstream issuer pin as outstanding when +`docs/evidence/2026-09-09-upstream-issuer-pin.json` showed it done ten hours +earlier — `check_before.issuer_matches: false`, then pinned to +`https://auth.coulomb.social`. Corrected in dd827b9f. The concern itself was +sound; only the claim that it was still owed was wrong, and the two are worth +separating rather than letting the correct half excuse the other. + +The lesson worth keeping: in a repository where several sessions work at once, +prose ages faster than the code and the receipts it describes. `docs/evidence/` +is the authoritative artifact for anything claiming a live proof here. Read it +before writing "outstanding", "unproven" or "not done" — that check is cheaper +than any of the corrections it would have prevented. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md new file mode 100644 index 00000000..392f9775 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md @@ -0,0 +1,81 @@ +--- +id: KEY-WP-0029 +type: workplan +title: "Reassess SCOPE against the current revision" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: scope-reassessment +created: "2026-09-09" +updated: "2026-09-09" +state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78" +state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78" +--- + +SCOPE.md declared itself reviewed against `b989de4`, 63 commits behind. Its +limits section had been kept current by appending to bullets, which works until +the header asserts a review date that no longer describes what was reviewed. + +## Recheck every claim against source + +```task +id: KEY-WP-0029-T01 +status: done +priority: medium +state_hub_task_id: "b12dfe7d-f4b6-5c44-89c6-9d3c02435d4c" +``` + +Routes read from the mux registrations, subcommands from the dispatch in +`main.go` and `internal/authclient/cli.go`, flags from the flag sets, claim +behaviour from `token.go` and `userinfo.go`. Where the existing prose and the +code disagreed, the code won. + +Three claims were false and are corrected: caller commands omitted +`verify-client`; the container was described as packaging only `keycape`, untrue +since KEY-WP-0026; and the issuer API row omitted `/readyz`. Capabilities that +had only reached SCOPE piecemeal — the authorization-code bindings, upstream +token verification, readiness and drain, the human tenant rules, +authentication-time assurance, the shared verifier — are now stated in the +capability table rather than only in the limits prose. + +## Separate source behaviour from deployed behaviour + +```task +id: KEY-WP-0029-T02 +status: done +priority: medium +state_hub_task_id: "28ae58c0-9bb9-573f-9cf3-51230d053e3c" +``` + +The document now says plainly that it describes source, and that the two are +different claims with different evidence: the test suite and harness runs for +one, committed receipts in `docs/evidence/` for the other. + +What is proven live is narrower and is stated exactly — the issuer pin, whose +pre-pin state did *not* match, so that risk was real rather than hypothetical; +and the two approval clients verified in an attended owner process. Both receipts +decline real predecessor rotation and observed wall-clock expiry, and SCOPE +declines them too. The deployed image predates this revision, so several +behaviours SCOPE describes are running nowhere yet, two of them fail-closed at +startup. + +This is the correction that matters. Four times in two days a statement about +current state was written from a workplan paragraph, a partial `git status`, or +an inference about what was admitted, without reading the authoritative artifact. +A SCOPE that does not distinguish "the code does this" from "this is running" +invites exactly that error from the next reader. + +## Record the reassessment + +```task +id: KEY-WP-0029-T03 +status: done +priority: low +state_hub_task_id: "7ef92d4f-eb56-5208-8b85-ed7bb2656c46" +``` + +`history/2026-09-09-scope-reassessment.md` records the method, what changed since +2026-09-05, the corrections, and what remains. It supersedes rather than replaces +the 2026-09-05 assessment, which stays as the record of the ten-gap backlog and +its closures; both are linked from SCOPE. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md new file mode 100644 index 00000000..0f74e0d5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md @@ -0,0 +1,73 @@ +--- +id: KEY-WP-0030 +type: workplan +title: "Make the static-registration precondition a checked condition" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: tenant-precondition-guard +created: "2026-09-09" +updated: "2026-09-09" +state_hub_workstream_id: "9ed8f851-080a-5d15-8642-f799c2e3295c" +--- + +`informed-decision` replied on KEY-WP-0013-T05 and asked for one thing that is +ours to do: the caveat under which a registration-bound human tenant is safe +should be "a condition of the capability, so a future change to registration +policy has to confront it", not reasoning left in a message. + +They are right that it was only prose. `docs/tenant-claim-contract.md` states it, +and a separate test asserted `registration_endpoint` is absent — but nothing +connected the two, so a future session adding dynamic registration would see a +test about discovery metadata, not a warning about relabelling users. + +## Tie the exclusion to the capability it protects + +```task +id: KEY-WP-0030-T01 +status: done +priority: medium +state_hub_task_id: "4729d591-39d4-5f33-bef8-db8057090913" +``` + +`TestRegistrationBoundTenantRequiresStaticRegistration` asserts both halves +together: that a client-declared tenant is issued, and that dynamic client +registration is not advertised. Whichever is removed first, the failure points at +the other. + +The failure message carries the reasoning rather than the observation. Adding +dynamic registration while a registration may declare its users' tenant means +anyone able to register a client can relabel the users who log in through it into +a zone of their choosing; the message says that and names the two ways out, drop +the capability or gate it to statically configured registrations. + +Verified in both directions rather than assumed: advertising a +`registration_endpoint` fails it with the escalation message, and neutering +`humanTenant` fails it with the message saying to remove the guard along with the +capability it protects. + +This is deliberately not a vote on the tenant question. It makes the precondition +of one option checkable; it does not choose between them, and if option 1 lands +the capability and this guard are removed together. + +## What stays with the owners + +```task +id: KEY-WP-0030-T02 +status: done +priority: medium +state_hub_task_id: "32b10327-f55d-5a0e-a4bc-be92929469dc" +``` + +`informed-decision` prefers option 2 and explicitly declines to treat that as the +answer, flagging it to gate-house on GH-DEC-2026-012 and naming approval-engine's +stake. KeyCape leans the same way, which is a reason for more care rather than +less: the code already implements option 2 (`329e48f`), so the doctrine question +is being decided against a working implementation. That is worth saying out loud +to everyone waiting on it, and was. + +Their scope set `[openid, approval:read, approval:approve]` is confirmed and +already published. `client_id` and callback follow at their T07, deliberately not +before the tenant question resolves — registering a client that fails closed at +first use is the failure this exchange existed to avoid. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md new file mode 100644 index 00000000..d2ede9d4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md @@ -0,0 +1,81 @@ +--- +id: KEY-WP-0031 +type: workplan +title: "Implement the gate-house tenant ruling" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: gate-house-tenant-ruling +created: "2026-09-10" +updated: "2026-09-10" +state_hub_workstream_id: "2bc4d085-9b10-5160-bc8a-4fccc5d41ba9" +--- + +gate-house ruled GH-DEC-2026-013 (`gate-house@16c1d46`) on the human-tenant +question KeyCape declined to settle for itself. Directory-sourced is the terminal +state; the registration-bound shape is granted as a declared bounded gap under +three conditions, on the general property that *a transitional shape is +admissible where it fails closed on exactly the case distinguishing it from the +correct resolution*. Ours refuses issuance where registration and directory +disagree, which is that case. + +Conditions (a) and (c) needed nothing from this repository — (a) is already the +implemented behaviour, and its strengthening, that a future change preferring the +**directory** is equally void because picking any winner is a silent +cross-tenant assertion, is recorded in the contract. This workplan covers the two +items that were implementation. + +## Carry the tenant claim's provenance + +```task +id: KEY-WP-0031-T01 +status: done +priority: high +state_hub_task_id: "46d42120-eca5-5b7f-8445-2211b3b8eab0" +``` + +gate-house ruled on GH-DEC-2026-013 while this was in flight. Section 5 is a +finding nobody asked for and is ours to implement: `tenant` is a bare string, so +a consumer cannot tell a zone the directory asserted about the person from one a +registration supplied about the client they came through. approval-engine +exact-matches that string while its contract reads as though it relies on the +first — the check is sound and the property a reader infers from it is absent. + +Every token now carries `tenant_source` beside `tenant`: `directory`, +`registration`, or `default`. It is advertised in `claims_supported` and asserted +on both grants at the token level, not only in the resolution function, because +the claim a consumer reads is the thing under obligation. + +Three values where the ruling names two, and that is the substantive judgement +here. Labelling an unasserted profile default as `directory` would reproduce the +same defect one level down — a consumer reading an assertion the identity layer +never made. The ruling cites GH-DEC-2026-011 §3 on unknown versus absent for the +case it examined; the same rule applies to our own fallback, so the fallback is +named rather than folded into the strongest neighbouring value. + +The agreement case resolves to `directory` deliberately: if a registration +declares the zone the directory also assigned, the directory did assert it, and +reporting the weaker source would understate what is known. + +## Condition (b) corrected a message I had just shipped + +```task +id: KEY-WP-0031-T02 +status: done +priority: high +state_hub_task_id: "b21aeac8-8370-5572-ab00-6e9812fa48e7" +``` + +The guard committed in `5f516a0` told a future reader that adding dynamic +registration could be resolved by dropping the capability **or gating it to +statically configured registrations**. Condition (b) voids the second: admitting +dynamic registration voids the registration-bound shape that day, and the +directory becomes the only source whatever state the adapter is in. gate-house +strengthened our own "must be revisited" on the grounds that *revisited* implies +the answer might survive review, and it would not. + +So the message offered a way out the ruling forbids, in the exact place someone +would read it while making that change. Corrected the same day. Worth recording +rather than quietly editing: the guard was written to force a confrontation, and +a guard that suggests an inadmissible resolution is worse than none. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md new file mode 100644 index 00000000..a04f52f8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md @@ -0,0 +1,75 @@ +--- +id: KEY-WP-0032 +type: workplan +title: "Pin the single route by which a principal becomes human" +domain: infotech +repo: key-cape +status: finished +owner: claude +topic_slug: principal-type-provenance-guard +created: "2026-09-10" +updated: "2026-09-10" +state_hub_workstream_id: "150b892c-f112-5fb9-b0f0-8072a52b85d5" +--- + +GH-DEC-2026-016 §5 applies A-16 to what makes a principal *human*: if `human` is +reachable by two routes — asserted by the identity layer about the person, or +supplied by a registration about the client they came through — the record must +say which, and a human-in-the-loop control must not be discharged on the +registration-supplied one. Refusing a service principal while accepting an +unverified assertion of humanity moves the defect rather than closing it. + +gate-house said explicitly this was not a request. It is built because the +property it protects is true *today* and is the kind a plausible change erases +quietly. + +## Pin the property, not the behaviour + +```task +id: KEY-WP-0032-T01 +status: done +priority: medium +state_hub_task_id: "2f42a72e-6292-5c02-9223-835db17b0a63" +``` + +`principal_type` has exactly one route to `human`: a string literal on the +authorization-code path, reached only after an upstream login resolved to a +directory user. No configuration field can assert it — verified against source +rather than assumed, and it is why the tenant provenance work has no counterpart +here and why A-16 does not yet bite. + +Asserting the *behaviour* would not have protected that. A test checking "a human +token says human" passes just as happily when the value starts coming from a +registration. So the guard parses the package and requires every assignment to +`principal_type` to be a string literal, with the literal set exactly +`{human, service}` — one route each. `claims["principal_type"] = +client.PrincipalType` would look like a feature in review and fails the build +instead, with a message saying that if it is deliberate the claim must carry its +provenance the way `tenant_source` does. + +Both assignment shapes are covered. The browser path assigns into a map; the +service path uses a key-value pair inside a map literal. Checking only +assignments found one of two routes and passed — a guard proving less than it +claimed, caught because the expected literal set was pinned rather than merely +counted. + +Verified by mutation: replacing the literal with a function of the client +registration fails it at the exact line, and restoring it passes. + +## What this deliberately does not do + +```task +id: KEY-WP-0032-T02 +status: done +priority: medium +state_hub_task_id: "13199970-75d1-5c38-aa8c-27d6e38dcf65" +``` + +No `principal_type_source` claim was added. There is one route, so a provenance +marker would encode a distinction that does not exist and invite consumers to +branch on it — the opposite of the tenant case, where two routes existed and the +claim could not say which had been taken. + +The guard is the honest response to "not a request today": it makes the absence +of a second route a checked fact rather than a remembered one, and hands the +decision to whoever creates the second route, at the moment they create it. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md new file mode 100644 index 00000000..5b478f95 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md @@ -0,0 +1,93 @@ +--- +id: KEY-WP-0033 +type: workplan +title: "Preserve fresh-user authentication for the Vergabe company handoff" +domain: infotech +repo: key-cape +status: finished +flavor: implementation +owner: codex +topic_slug: netkingdom +created: "2026-09-12" +updated: "2026-09-25" +related: [VERGABE-WP-0019, NK-WP-0037, USER-WP-0025] +state_hub_workstream_id: "144dd430-1a09-51e1-9aad-e799ee86c338" +--- + +## Forward login freshness to the actual authentication provider + +```task +id: KEY-WP-0033-T01 +status: done +priority: high +state_hub_task_id: "fe05486b-f079-503b-ade1-fca65bf64b34" +``` + +KeyCape parsed prompt=login and max_age but dropped them before Authelia. +Forward these through the provider-neutral AuthRequest and the Authelia adapter. +Absent values retain ordinary SSO behavior. Handler and adapter regressions +cover forced login and zero/nonzero maximum age. Full Go suite passes after +correcting the existing example-count regression: the example file already +contains four service clients and the admitted human approver client. + +## Publish, preflight and prove the fresh recipient boundary + +```task +id: KEY-WP-0033-T02 +status: done +priority: high +state_hub_task_id: "7b58f08c-6063-554a-8700-a1edbc805ca4" +``` + +Publish the exact source and validate current live config before replacing the +single issuer instance. The live image is dcebd46/digest 7ff54c54; current main +also contains startup validation and tenant provenance changes documented in +docs/operations.md. Preserve all unrelated client, credential and MFA policy +configuration. NK-WP-0037 adds the exact public Vergabe callback with no tenant +assertion or MFA downgrade. Confirm prompt=login reaches Authelia through the +live redirect, wrong callbacks and missing PKCE fail, and the actual recipient +uses their own identity. Provider restart invalidates pending in-memory logins; +USER-WP-0025-T03 still owns full provider sign-out coordination. + +2026-09-12 release prepared: source 8d4336e, Forgejo image run 45 succeeded, +digest sha256:5f10f36a5da23ce1aaf3df9b84a8ff98d7926f34ceaa19e63bd3356adb68e01a. +The exact client-registration and deployment server dry runs pass. The runtime +is unchanged. Await the attended window required by docs/operations.md; the +complete packet is railiance-apps/docs/vergabe-demo-company-sso-rollout.md. + +2026-09-12 attended rollout executed after explicit operator approval. KeyCape +and password setup are Ready on the prepared digests; exact public client +registration was CAS-applied (config resourceVersion 60123977) with unrelated +config bytes/Secret data preserved. Existing portal and product client both +pass fresh-login forwarding, wrong-callback and missing-PKCE checks (6 checks). +Vergabe Helm revision 2 is Ready; identity migration completed, both PVCs remain, +and requests remain 60m CPU/256Mi memory. Eleven live product checks pass: +company welcome, anonymous gate, no-store, secure scoped CSRF, POST/CSRF-only +login start, native issuer redirect, private company/media protection and +invalid callback/confirmation rejection. Initial readback showed zero accounts, +identity mappings and staff accounts. Native invited-user sign-in/MFA and +confirmation are now requested from the operator; no user credential was used +by the agent. Recovery and two-user acceptance remain their existing tasks. +Evidence: railiance-apps/docs/evidence/2026-09-12-demo-company-sso-live.md. + +2026-09-23 regression from the attended recipient sign-in (net-kingdom message +ee4808e2, NK-WP-0037-T02): Authelia 4.38 registers the authorization request +only after the login, so forwarding prompt=login fails every real login ("auth_time +happened before the authorization request was registered"). The 2026-09-12 +forwarding checks were redirect-only and never completed a login. Fix (owner +option a): KeyCape no longer sends prompt to Authelia. prompt=login and +max_age=0 travel as max_age=10, a shorter max_age is raised to 10, a longer one +passes through. KeyCape then enforces the exact requirement itself against the +verified upstream auth_time: prompt=login needs an authentication no earlier +than the KeyCape request, max_age needs one within that age, and a missing +auth_time fails closed (event `stale_upstream_authentication`). Known edge: a +user whose Authelia login is under 10s old at request time is not re-prompted +and is refused rather than admitted. Also fixed the stale example-count test left +by 651625c/1620ce2 (6 service + 1 human). Full Go suite passes. + +The image was already the live issuer +(`sha256:8dc35801`, Forgejo run 69, source `11ce29a`). No second rollout. +2026-09-24 23:46 UTC the founder completed the Vergabe journey: one +`token_exchange_error`, then `auth_success` and `token_issued` for +`vergabe-demo-company`. No `stale_upstream_authentication`. Receipt: +`docs/evidence/2026-09-24-fresh-login-and-account-switch.md`. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0034-account-recovery.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0034-account-recovery.md new file mode 100644 index 00000000..eb58f99b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0034-account-recovery.md @@ -0,0 +1,64 @@ +--- +id: KEY-WP-0034 +type: workplan +title: "Browser authentication recovery and confirmed shared sign-out" +domain: infotech +repo: key-cape +status: finished +flavor: implementation +owner: codex +topic_slug: key-cape +created: "2026-09-12" +updated: "2026-09-25" +state_hub_workstream_id: "49b0cd1d-fb9d-5351-99eb-9be9143a092d" +--- + +The operator reports a dead-end authentication error after using an account +outside the product tenant. Recent issuer telemetry indicates token exchange +failure; tenant rejection and provider failure must not be conflated. + +## Implement and validate recovery + +```task +id: KEY-WP-0034-T01 +status: done +priority: high +state_hub_task_id: "ba285335-4ead-5478-8fb7-89a2ec1ba695" +``` + +Route failed browser login to the public account recovery surface without codes, +state or unverified identity. Show verified portal identity, tenant memberships, +and recorded workload memberships; preserve operator/customer separation. +Provide CSRF-protected portal logout and confirmed shared provider sign-out with +fixed owner-configured return locations. No automatic reauthentication loops, +MFA downgrade, global JWT revocation claim or inferred workload entitlements. + +## Publish and verify the recovery flow + +```task +id: KEY-WP-0034-T02 +status: done +priority: high +state_hub_task_id: "4c836ddf-2eb8-528a-b63f-8499f3a6cbf2" +``` + +Publish immutable images, update canonical runtime pins, verify anonymous +recovery and sign-out confirmation live, and record actual account switching +only after browser evidence. Existing application sessions may outlive provider +logout. Related: USER-WP-0025-T03 and VERGABE-WP-0019-T06. + +Source verification: Full Go suite passed; final OIDC/config tests passed after recovery expansion. Immutable publication and live checks are in progress. + +### Live recovery rollout — 2026-09-12 + +Recovery is deployed in KeyCape 4d8b8fe, User Engine e54b6ee and Vergabe c067993 +(Helm revision 3). All three are Ready. Six provider checks, eleven product +checks and six fresh anonymous Chromium checks pass, including actual provider +logout POST and return to the portal without test overrides. Detailed receipt: railiance-apps/docs/evidence/2026-09-12-account-recovery-live.md. + +2026-09-24 23:42–23:45 UTC the founder completed the account switch. The issuer +recorded one `mfa_failed` for `user-engine-portal`, then three separate +`auth_start` → `auth_success` → `token_issued` sequences. A silent reuse of +the previous portal session would not have produced those new successes. +Provider logout still does not revoke already issued application JWTs. +Receipt: `docs/evidence/2026-09-24-fresh-login-and-account-switch.md`. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0035-optional-mfa.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0035-optional-mfa.md new file mode 100644 index 00000000..29eb711f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0035-optional-mfa.md @@ -0,0 +1,112 @@ +--- +id: KEY-WP-0035 +type: workplan +title: "Opt-in MFA for demo-company login" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: infotech +created: "2026-09-13" +updated: "2026-09-14" +state_hub_workstream_id: "b27601d8-d0a5-5c79-a1ff-c2a4cedafa74" +--- + +Requested behavior: password-only login before OTP activation; require OTP after +activation. Do not lower assurance of unrelated applications. + +## Implement enrollment-dependent browser policy + +```task +id: KEY-WP-0035-T01 +status: done +priority: high +state_hub_task_id: "c1ed06c2-c689-53d1-afc7-e10d79e9e94f" +``` + +Add mfaOptional, preserve explicit AAL2 and existing client policy, fail closed on +provider lookup errors and malformed/incomplete responses. Preserve the setting +in config/runtime/canonical model and flag manual migration requirements. +Validation: `go test ./...` and `git diff --check` passed on 2026-09-13. See docs/optional-mfa.md. + +## Restore authoritative factor lookup and enable the reviewed client + +```task +id: KEY-WP-0035-T02 +status: done +priority: high +state_hub_task_id: "b066a273-f91c-50ee-b497-a39eace03b3a" +``` + +Factor lookup restored on 2026-09-13 through the new dedicated service lane +RPF-WP-0040 / CCR-2026-0023. Native ESO delivery, scoped provider user lookup, +renewal and mounted replacement passed. KeyCape now uses adminTokenFile; other +configuration and current MFA policy were preserved. Historical NK-WP-0033 +resolver lanes remain separate. The remaining T02 work is the exact scoped +client migration and no-factor/enrolled/error/step-up acceptance before enabling +optional policy. See railiance-platform/docs/evidence/2026-09-13-keycape-factor-custody.md. + +## Verify optional enrollment and account management access + +```task +id: KEY-WP-0035-T03 +status: done +priority: high +state_hub_task_id: "93ce2e97-6c5a-58a7-8f3c-1c4f0eb6f1b5" +``` + +Verify provider self-service login, possession-confirmed activation, cancellation +and removal/recovery. Resolve shared portal assurance scope before surfacing the +verified OTP setup link. Actual user login acceptance remains open under +KEY-WP-0034 and VERGABE-WP-0019; this work does not finish either workplan. + +## Support bounded provider credential renewal without issuer restart + +```task +id: KEY-WP-0035-T04 +status: done +priority: high +state_hub_task_id: "494f554d-6542-5797-804f-6df4a05dcadb" +``` + +Supports platform journey P05 and USER-WP-0030-T03. Add an exclusive mounted adminTokenFile credential source, fresh reads for lookup and validation, no stale fallback, bounded input, sanitized failures and atomic replacement acceptance. Preserve inline configuration compatibility. Require provider success and true validation value before AAL2; bound default request time and reject credential-bearing redirects. Test renewal, rejection, recovery and policy regressions. This implements the consumer delivery contract; owner credential issuance/custody and live P04/P05/P06 acceptance remain T02/T03. + +All Go regression/conformance suites pass, including five new renewal/validation tests with invalid-source subcases. Added exact-commit authentication acceptance CI. Provider-mounted credential delivery and effective optional policy remain gated separately. + +Consumer source 632b1f1 deployed and Ready 1/1; CI, four provider HTTP checks and twelve live browser checks passed. See docs/credential-renewal-release-2026-09-13.md. T02/T03 retain actual credential delivery and live recovery/policy acceptance. + +2026-09-13 custody activation supersedes the earlier owner-handoff gate: the new factor service is live and renewable. T02/T03 now track effective policy/onboarding acceptance, not missing credential ownership. + +## Require confirmed enrollment and actual OTP evidence + +```task +id: KEY-WP-0035-T05 +status: done +priority: high +state_hub_task_id: "94f579d1-a7af-564c-ab0c-ae76fce343aa" +``` + +Live provider source inspection found active tokens can remain in verification +state; live policy inspection found password passthrough enabled for users without +factors. Distinguish pending enrollment from completed enrollment and require +TOTP/HOTP serial/type evidence before AAL2. Cover pending/confirmed/cancelled, +existing-factor plus pending enrollment, missing/unknown state, incomplete pages, +password passthrough and static-password token rejection. All Go suites pass +locally; source 113f3a6 was published, CI acceptance/build passed, and the guarded issuer replacement is live. See docs/enrollment-assurance-release-2026-09-13.md. + +Provider recovery testing additionally proved that withdrawing reader policy +returns HTTP 200 with an empty list. Added and deployed a same-realm disabled, +unassigned visibility probe before no-factor decisions, with missing visibility +failing closed. Native scope proof passed. Isolated installed-provider tests +passed possession confirmation/cancellation, permission withdrawal/recovery and +provider-issued JWT expiry; these are now included in CI. T02/T03 remain the +actual optional-client and self-service browser activation gates. + +2026-09-14: T02/T03 completed through USER-WP-0033 P06. Installed-provider +native and five browser checks passed, including confirmed activation, +cancellation, old-session MFA and denied active-factor replacement. Portal +guards, provider restrictions and both scoped optional-client policies are live; +PVC readback confirms effective state. Full release evidence and rollback: +`../user-engine/docs/evidence/2026-09-13-p06-authentication-policy.md`. +Residual actual-user and workload sign-in acceptance remains live under +KEY-WP-0034, USER-WP-0028 and VERGABE-WP-0019 before this closure. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0036-platform-factor-recovery.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0036-platform-factor-recovery.md new file mode 100644 index 00000000..fd296bc0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0036-platform-factor-recovery.md @@ -0,0 +1,83 @@ +--- +id: KEY-WP-0036 +type: workplan +title: "Audited platform administrator lost-factor recovery" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: infotech +created: "2026-09-13" +updated: "2026-09-13" +state_hub_workstream_id: "f9ba00a6-5758-5677-9a49-da1e35875f7a" +--- + +Implements the provider part of P04 under USER-WP-0030-T03. The existing factor +read credential is deliberately not elevated to recovery authority. Recovery +now has a private signed-OIDC service and platform-only portal browser flow. + +## Implement bounded and audited provider recovery + +```task +id: KEY-WP-0036-T01 +status: done +priority: high +state_hub_task_id: "f3287544-8412-5b1f-9d16-9f7b9e895ceb" +``` + +`scripts/factor_recovery.py` previews exact ownership and shared-identity scope, +requires an operator verification attestation and matching metadata version, +disables one factor with a row lock, requires pre-mutation audit durability, +reads back state, records completion and reconciles retries. Reject changed or +replaced factors and reference conflicts. Nine isolated unit tests pass, including replacement between disable and readback. +The installed-provider Job provider-recovery-contract-02 passed real database +and audit persistence, disable/readback and replay assertions in an isolated +fixture. Acceptance CI includes both suites. No real user's factor was changed. + +## Validate the attended owner entry point + +```task +id: KEY-WP-0036-T02 +status: cancel +priority: high +state_hub_task_id: "2d59da13-782e-5aeb-8957-69e41a15ddb1" +``` + +railiance-platform/scripts/keycape_factor_recovery.py derives actor from an +OpenBao platform-admin entity, rejects root/workload substitutes, checks cluster +identity, and emits only a private metadata receipt. Runbook: +railiance-platform/docs/keycape-factor-recovery.md. Offline owner tests pass. +Superseded: routine recovery now independently verifies the platform operator's +signed KeyCape session through the private service. The extra OpenBao/Kubernetes +operator front door is not part of the supported P04 journey; its native acceptance +is cancelled rather than falsely marked passed. No credential handoff is pending. + +## Integrate platform support recovery with the browser journey + +```task +id: KEY-WP-0036-T03 +status: done +priority: high +state_hub_task_id: "f7248d5e-6f68-5637-ada5-2527436f5f9e" +``` + +Implement an authenticated bounded recovery service and platform-only portal +flow with fresh assurance, server-derived actor, CSRF, ownership-verification +process, exact target/scope confirmation, audit/reference readback and safe +retry. Never grant Kubernetes access to the portal. Test unauthorized/tenant +admin denial, stale confirmation, provider failure, retry and replacement OTP +enrollment using disposable identities. Mandatory-AAL2 access is restored only +after possession of a replacement factor is proven. Keep P04 incomplete until +browser success, failure and recovery paths pass; CLI tests alone do not close it. + +Completion evidence: user-engine/docs/evidence/2026-09-13-p04-recovery.md. +KeyCape 63b3070 and portal c298cd9 are released. Fifteen recovery tests, 219 portal +regressions with PostgreSQL and 19 Chromium checks pass. Installed-provider +provider-p04-contract-03 verifies signature/role/assurance denials, confirmation, +multi-owner refusal, ownership locking, audited mutation/retry/reference lookup and replacement OTP possession/login. +Production health, anonymous/invalid-token denials, signing-key connectivity, +rollout and new-enrollment policy readback pass. Positive mutation used disposable +fixtures; no real account was changed. Provider and portal implement the same +supported recovery contract; no outstanding P04 implementation remains. +Broader policy/self-service and notification operations remain live under +USER-WP-0030-T03 and KEY-WP-0035, not residuals of this bounded recovery plan. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md new file mode 100644 index 00000000..ad7fed77 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md @@ -0,0 +1,135 @@ +--- +id: KEY-WP-0037 +type: workplan +title: "Secrets Engine OpenBao live registration and protected custody" +domain: infotech +repo: key-cape +status: finished +owner: codex +topic_slug: netkingdom +created: "2026-10-04" +updated: "2026-10-07" +state_hub_workstream_id: "62800ecf-064b-5546-a1e4-4521b5932973" +--- + +KeyCape's accepted provider contract (KEY-WP-0009) does not establish actual +registration or custody. This plan names those missing owner gates for +RPF-WP-0035-T02. The founder authorized workers to attend the review requests, +not live registration, credential generation, rotation, fetching or deployment. + +## Establish the exact registration and deployment authority + +```task +id: KEY-WP-0037-T01 +status: done +priority: high +state_hub_task_id: "0409f656-65fb-5ee2-a934-a687a3e139f1" +``` + +Reviewed KeyCape base 672e1faf4b3401ff7bf68bb0b29f8dc50ca51ac6: +`config/service-clients.example.yaml`, `docs/openbao-service-auth-contract.md`, +`src/internal/config/config.go`, `src/cmd/keycape/clients_test.go`. +The exact service registration remains accepted: secrets-engine-openbao, +service:secrets-engine, tenant:platform, role secrets-engine, only openbao:login, +client_credentials, confidential, tokenLifetime 15m and issuer +https://kc.coulomb.social. Audience defaults to clientId, with no resource +audience override. See the owner packet for concrete deployment/custody proposals. +NetKingdom owns the live sso/keycape deployment and config, not this example. +Existing approved approval-client rollout demonstrates the owner procedure; +it does not approve or provision this different service client. + +## Agree and prepare the separately approved protected custody operation + +```task +id: KEY-WP-0037-T02 +status: done +priority: high +state_hub_task_id: "ac798fb4-ed77-5e45-83ab-f56eea92be34" +``` + +Platform owns the authoritative OpenBao client-secret custody and least-privilege +issuer-side delivery; secrets-engine owns its explicitly selected mode-0600 +client-side file outside every worktree. The proposal in +`docs/secrets-engine-openbao-owner-packet.md` provides candidate coordinates, +not a live custody claim or allocated CCR. Complete the platform CCR(s), delivery +policy/roles, two target bindings, exact attended child and rollback before +seeking the founder's separate concrete approval. Values stay out of Hub/Git/chat, +argv and logs. Existing verifier/requester values must not be reused. + +**Done 2026-10-05.** The founder resolved `632a8893-0d6a-4591-a50b-40bbdb798f32`. +The custody boundary is approved: platform owns OpenBao client-secret custody +and issuer-side delivery, and secrets-engine owns the mode-0600 file outside +every worktree. The owner packet is still coordinates. This approval does not +register the client, generate a secret, or deploy. + +## Prepare and approve the deployment-owned registration operation + +```task +id: KEY-WP-0037-T03 +status: done +priority: high +needs_human: true +decision_id: caafe4be-4f68-47b4-9567-316d47b94992 +blocking_reason: "Custody boundary 632a8893 is approved. Registration merge and rollout still need an exact reviewed revision and a separate live go. Opening caafe4be does not deploy." +state_hub_task_id: "52f03e39-d05b-5da5-85df-e28c059430e4" +``` + +Coordinate source registration merge and the single new env secretKeyRef with +NetKingdom's `sso-mfa/k8s/keycape/deployment.yaml` and owner rollout implementation. +An exact reviewed source revision, image digest, config preservation test, +synthetic issuance/denial verification and founder live go are required. +Use an idempotent exact-match merge: absence permits adding only this client; +matching client permits noop; duplicates or drift fail closed. Preserve signing +key, unrelated clients, existing policy and confidential config bytes. +No deployment changes are authorized by this source plan. + +**Done 2026-10-07 under live go `caafe4be`.** +- Custody (decision `2ab717e1`): KV v1 written with cas=0. +- ESO amendment `1fdd993b`; the sso Secret is synced. +- NK-WP-0046 (`1fbb8db`): env binding, then the exact-match config add + (rv 71471711 to 71655453, unrelated bytes preserved), then a restart on image + 6f79a2af. + +**Prepared 2026-10-06 (source only).** NetKingdom NK-WP-0046 (`a1e7302`) holds +the exact-match single-client merge helper, its rollback and the proposed env +patch. secrets-engine SECRETS-WP-0012 (`a3897c2`) names the consumer file. +Platform CCR-2026-0033 (`df24a77`, still proposed) holds the KV path +`platform/workloads/keycape/secrets-engine-openbao`, the sso-only ESO and the +attended custody script; its approval is RPF-WP-0035-T09. The live order is +custody run, then issuer Secret, env patch, this merge and a restart. Nothing +is live. The founder keeps the deployed image (NK-WP-0046-T02, 2026-10-06). +Revised the same day: the value lives only in OpenBao (ops-mason MASON-WP-0009) +and has no consumer file. The custody approvals A1-A3 are decision +`2ab717e1-7881-491e-aed4-28946550c289`. + +## Verify actual issuance and publish non-secret owner receipt + +```task +id: KEY-WP-0037-T04 +status: done +priority: high +depends_on: + - KEY-WP-0037-T03 +blocking_reason: "Live issuance proof waits on the registration operation (T03)." +state_hub_task_id: "40b80bcd-243d-5bf8-ba18-b992e3c5f8f7" +``` + +After authorized registration and custody, verify issuer/JWKS signature, subject, +audience, principal_type, tenant, roles, scope and 900-second lifetime; refuse +wrong credential/excess scope and preserve existing human login. Record only +booleans and identities, immutable image/source coordinates and custody binding +metadata. Hand off to RPF-WP-0035-T02 for isolated OpenBao metadata provisioning, +claim/expiry/use-limit denial and self-revocation proof. Never report an example +registration, discovery metadata or source test as live acceptance. + +**Done 2026-10-07.** `keycape verify-client` passed all 10 checks: +- issuer, JWKS signature and claims (`service:secrets-engine`, tenant:platform, + role secrets-engine); +- no excess scope; +- `openbao:admin` and a wrong secret refused. + +The founder's KeyCape login after the restart also proved the CCR-2026-0034 +operator reader (net-kingdom-admins, aal2). Receipt: +`docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json`. The 900s lifetime +was not measured. Handed to RPF-WP-0035-T02 for the OpenBao JWT role and login +proof. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0038-forgejo-oidc-client.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0038-forgejo-oidc-client.md new file mode 100644 index 00000000..8cd5bc66 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0038-forgejo-oidc-client.md @@ -0,0 +1,212 @@ +--- +id: KEY-WP-0038 +type: workplan +title: "Forgejo as a NetKingdom OIDC client, so the forge can close" +domain: infotech +repo: key-cape +status: active +flavor: implementation +owner: claude-code +topic_slug: railiance +created: "2026-10-08" +updated: "2026-10-16" +depends_on: [] +related_repos: + - prj-forgejo-org-refactor + - railiance-apps + - railiance-platform +state_hub_workstream_id: "098e3e2c-a138-560e-b2d3-3a15eb49d8a9" +--- + +# KEY-WP-0038 - Forgejo as a NetKingdom OIDC client, so the forge can close + +## Why + +`forgejo.coulomb.social` is closing: private by default, no anonymous access +(prj-forgejo-org-refactor ORGREF-WP-0002, decision +`decisions/forge-visibility-policy.md`). The founder's condition is that Forgejo +sign-in runs through NetKingdom **before** anything closes, so that every human, +agent and service reader has a governed identity rather than an ad-hoc local +account. This plan is ORGREF-WP-0002-T04. Its Forgejo-side counterpart is +railiance-apps `RAPPS-WP-0016` (ORGREF-WP-0002-T05), which owns the Forgejo +Helm release. + +## Boundaries + +- Registration follows the existing human browser-client pattern + (`docs/approval-engine-provisioning-request.yaml`, `human_registration`) and the + client fields validated in KEY-WP-0028. Redirect URIs are exact, never + wildcards. +- The client secret goes to OpenBao through railiance-platform custody. No secret + value in git, State Hub, logs or chat. +- Founder guardrail for the whole closure: tighten gradually, never lock anyone + out, never risk assets. Nothing in this plan changes who can read the forge + today. It only makes the NetKingdom path available. + +## T01 — Define what Forgejo receives + +```task +id: KEY-WP-0038-T01 +status: done +priority: high +state_hub_task_id: "5f019d13-1fa3-5b88-a27a-75a3339ca96e" +``` + +Write the claim contract Forgejo consumes, as a non-secret request packet under +`docs/`: + +- issuer `https://kc.coulomb.social`; confidential client `forgejo`; authorization + code with PKCE; scopes `openid profile email groups` +- redirect URI + `https://forgejo.coulomb.social/user/oauth2/netkingdom/callback`. The + authentication-source name `netkingdom` must match railiance-apps' + configuration exactly. +- subject and username: canonical directory subject, plus a stable + `preferred_username` that Forgejo can match to the existing local accounts + (founder and agent accounts) for linking +- `groups` naming the Forgejo organisations and teams to map to (for example + `forge:coulomb:owners`, `forge:coulomb:readers`), and `principal_type` so that + human and service principals stay distinguishable +- MFA: forge administrators and owners require assurance with a fresh second + factor; plain readers follow the client-aware MFA policy (KEY-WP-0008) + +Acceptance: the founder accepts the packet. The group names are agreed with +railiance-apps. + +2026-10-08: packet drafted at `docs/forgejo-oidc-client-request.yaml`. It covers a +confidential client `forgejo` with PKCE and `mfaRequired: true`, the exact +callback, scopes, a custody path, the claims as emitted at this revision, new +LLDAP groups `forge-admins`, `forge-coulomb-owners` and `forge-coulomb-readers` +with a required-claim gate, no automatic account creation, and linking of local +accounts. It also proposes that agents and services use scoped tokens bound to +service identities, not browser sign-in. + +2026-10-08: accepted by the founder (in session). railiance-apps' RAPPS-WP-0016 +uses the same authentication-source name `netkingdom` and the same group names, +so the agreement condition holds. Before T02, the LLDAP groups `forge-admins`, +`forge-coulomb-owners` and `forge-coulomb-readers` must exist, with the founder +in the first two. + +## T02 — Register the client and hand over its secret + +```task +id: KEY-WP-0038-T02 +status: wait +depends_on: [KEY-WP-0038-T01, NK-WP-0048-T06] +blocking_reason: "Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout." +priority: high +state_hub_task_id: "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30" +``` + +**Changed 2026-10-08 (founder direction):** no case-specific helper script. +Registration is the first case of the SecurityCaseType `oidc-browser-relying-party` +(net-kingdom NK-WP-0048, canon `security-case-types_v0.1`). The parameters +accepted in T01 become the case; ops-mason materializes it (MASON-WP-0010) and +ops-warden follows through (WARDEN-WP-0044). The registration capability is +net-kingdom's (`net-kingdom.keycape-client-registration`); key-cape's part is its +field rules and reviewing the type's composition. + +The original intent below still describes what must result. + +Register `forgejo` from the packet. Mint the client secret straight into custody +(OpenBao path `platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, derived +by the case type since net-kingdom 074b3a0; custody owner railiance-platform, +consumers KeyCape via ESO and Forgejo). Record a receipt with no +secret value. + +Acceptance: discovery metadata lists the client's redirect URI. The secret is +readable by Forgejo's custody path only. + +2026-10-10: key-cape reviewed the type's composition +(`docs/oidc-browser-relying-party-type-review.md`). net-kingdom fixed the findings +in `61ab6bc`, a 1.0 patch: redirects with `?` or `#` refused, the groups scope +required, and descriptions corrected. A new 1.1 (`e2174ce`, founder go) adds the +`consumer-enforces-groups` verification. For Forgejo, that is the required-claim +setting railiance-apps configures. `security-cases/forgejo-netkingdom-oidc.yaml` +is re-pinned to @1.1, with parameters unchanged. KeyCape added `keycape +check-config` (`c528daa`), which register-client runs before the restart. T02 still +waits on NK-WP-0048-T06, which needs both executors' light paths (MASON-WP-0010-T03, +WARDEN-WP-0044-T03). + +### Exact offline checker owner review — 2026-10-16 + +KeyCape owner review under KEY-WP-0038-T02 endorses only the offline config +validation behavior of source `3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, +`src` tree `f0010b9cab17dbc3a787628a29657d98b900320f`, compiled artifact +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. +The exact source/archive, 108 source files, 9876 cached Go 1.23.0 toolchain +files and all ten selected module trees matched the candidate closure. Fourteen +actual artifact checks and the cmd/config/policy Go tests passed using only +synthetic inputs. The check dispatches before private-key loading, upstream +adapter construction or server startup and uses placeholder client secrets. + +[Owner review](../docs/offline-checker-owner-review-2026-10-16.md) records the +exact evidence and limits: non-secret portal/logout URL environment overlays, +optional/nonempty env-name assertion and policy-store assertion, permissive YAML +loading, and no key/custody/readiness or authentication proof. It does not endorse +the caller's inherited environment, executable storage/custody, the NetKingdom +descriptor/type, Warden admission or any browser/native identity. Parent review +identified caller environment inheritance; NetKingdom owns its correction and +regression checks. T02 remains `wait`; T03 and all live acceptance gates remain +unchanged. No live config, credentials, cluster objects or paid calls were used. + +## T03 — Prove the claims for each kind of identity + +```task +id: KEY-WP-0038-T03 +status: wait +depends_on: [KEY-WP-0038-T02] +blocking_reason: "Claims are verified against the registered client." +priority: high +state_hub_task_id: "40ddaa29-7245-508c-ad48-ddc063e25115" +``` + +Before Forgejo uses the client, verify the issued ID token and UserInfo for one +human (the founder, with MFA), one agent identity and one service identity: +subject, `preferred_username`, `groups`, `principal_type`, assurance. Record the +claims, minus tokens, as evidence. + +Decide with railiance-apps whether agents and services sign in to Forgejo at +all, or receive scoped Forgejo tokens bound to a NetKingdom service identity +(ORGREF-WP-0002-T06). Record the decision. + +Acceptance: evidence for all three principals; the agent and service decision is +recorded. Then hand over to RAPPS-WP-0016. + +## T04 — Implement the `keycape-client` entry point + +```task +id: KEY-WP-0038-T04 +status: cancel +priority: high +state_hub_task_id: "583cf1f1-16dc-515f-83bc-1efc45024a89" +``` + +**Cancelled 2026-10-08: moved to net-kingdom NK-WP-0048-T09 (done).** The live +registration machinery (contained kubectl, cluster and issuer pins) already lives +in net-kingdom `sso-mfa/k8s/keycape/`, where KeyCape is deployed; a copy here +would drift. The entry point is `net-kingdom/sso-mfa/k8s/keycape/keycape-client.py` +and the declaration is `net-kingdom.keycape-client-registration`; key-cape's draft +declaration is removed. key-cape still owns the client field rules (canonical +model) and their enforcement at authorization time. + +`capabilities/playbooks/key-cape.oidc-client-registration.yaml` (contract PASS, +`maturity: planned`) declares client registration as a playbook capability, so +that the SecurityCaseType `oidc-browser-relying-party` (net-kingdom NK-WP-0048) +can compose it. Implement the entry point it names, `tools/keycape-client/`: + +- `keycape-client register --client-file ` with modes + `inspect`, `dry-run`, `apply` and `remove`; adds, updates or removes exactly one + client in the live registry; idempotent; never prints or writes secret values; + a confidential client's secret is read from its custody reference +- one implementation replacing the per-client helpers in net-kingdom + `sso-mfa/k8s/keycape/` (register-coulomb-social, portal-client-rollout, + vergabe-client-rollout, secrets-engine-openbao-client-rollout), keeping their + safeguards (byte-preserving merge, compare-and-swap, cluster pin, safe receipts) + and their tests as regression cases +- client field validation by the KEY-WP-0028 rules +- then set the declaration's `maturity` to `implemented` + +Longer term, loading clients from a non-secret registry with secrets referenced +from custody would remove the type's only attended step (see the type README, +"Attended steps"). diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md new file mode 100644 index 00000000..fd0fc46e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md @@ -0,0 +1,364 @@ +--- +id: KEY-WP-0039 +type: workplan +title: "Negative-check tokens for the secrets-engine OpenBao JWT role" +domain: infotech +repo: key-cape +status: active +flavor: implementation +owner: claude-code +topic_slug: netkingdom +created: "2026-10-10" +updated: "2026-10-17" +depends_on: [] +related_repos: + - railiance-platform + - secrets-engine +state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86" +--- + +# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role + +## Why + +CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role +`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong +`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT +and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine +`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked +whether KeyCape can issue the negative tokens without touching the +`secrets-engine` registration. + +KeyCape signs only what a registration declares. It has no way to mint a token +with arbitrary claims, and adding one would create a forging path in the issuer. +So the negative tokens must come from real, separate registrations that differ +from `secrets-engine` in exactly one property each. + +## Boundaries + +- The `secrets-engine` registration is not changed. +- Probe clients are confidential `client_credentials` clients with the minimum + 60-second lifetime, scopes that grant nothing, and secrets minted into custody + like any other client. They are removed after the proof. +- The probe script prints only accept or refuse booleans per case, never tokens or + secrets. It runs attended. + +## T01 — Define the probe registrations + +```task +id: KEY-WP-0039-T01 +status: done +priority: medium +state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8" +``` + +Write a non-secret request packet under `docs/` with one probe client per claim +mismatch, each identical to `secrets-engine` except for: + +- `aud`: a different access-token resource audience +- `sub`: a different service subject +- `tenant`: a tenant other than `platform` +- `roles`: a role other than `secrets-engine` +- `scope`: a scope outside the role's bound scope + +Two cases need no probe client. A service token always carries +`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong +`principal_type` case uses a human access token from an attended login. The +coding-agent case uses the existing `codex-railiance-platform` registration (role +`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live; +T01 confirms that. + +The expired-JWT case reuses any probe client: request a token, wait past its +60-second lifetime, then present it. + +Acceptance: railiance-platform agrees the packet covers every refusal that +CCR-2026-0038 names. + +2026-10-10: railiance-platform agreed that the case list covers every +CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no +provider fallback) are theirs and secrets-engine's. Packet written at +`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two +ways, both sent back for re-agreement: + +- **Expired case.** A token from another probe already fails a claim, so its + refusal does not show that expiry was checked. It needs a sixth client, + `openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That + makes it a second credential able to log in as secrets-engine for the session. + OpenBao's `expiration_leeway` (150s by default) also extends the wait. +- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is + set. Every probe except the aud probe therefore pins + `audience: secrets-engine-openbao`, so it fails only on its own claim. + +Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's +environment, because KeyCape resolves `secretRef` at startup. The packet uses a +transient Kubernetes Secret that is deleted in the same session. + +## T02 — Register the probe clients, run the attended probe, then remove them + +```task +id: KEY-WP-0039-T02 +status: wait +depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03] +blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved." +priority: medium +state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3" +``` + +2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. + +Write the attended script, which prints booleans only. + +2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in +`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused +boolean per case plus the refusal class (`claim_mismatch`, `expired`, +`not_service_principal`), and no token, secret, claim value or OpenBao error text. +A transport failure or an unexpected response is reported as `inconclusive`, never +as a refusal. A token that OpenBao accepts is revoked at once and fails the run. +Still waiting: railiance-platform's re-agreement on the two departures above, and +an attended registry session with net-kingdom. Register the clients +through the normal registration path, hand the run to railiance-platform, record +a value-free receipt in `docs/evidence/`, and deregister the clients afterwards. + +Acceptance: every case is refused, the receipt is committed, and the probe clients +are gone. + + +2026-10-11 source preparation: T01 remains progress because the exact expiry +client and audience-pin amendments require platform-owner re-agreement; the +prior agreement covered the earlier case list, not this revised packet. The +prepared runner revision adds a valid expiry positive control and immediate +revoke, exact decoded claim-isolation preflight (not signature evidence), +semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen +local tests pass; original-source semantic regressions demonstrate two false +successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner +returns, live authority, effective leeway, custody boundaries and cleanup. No +live token, registry or OpenBao action occurred. T02 continues to wait for owner +re-agreement plus separate attended live authorization. MASON-WP-0009-T05 +operator/ESO capability and nonmember checks remain independent acceptance. + +2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans. + + +2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner +acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md) +confirms all 14 executable/test/producer-decision pins and three Platform source +pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing +source acceptance, resolved registration go caafe4be and the standing attended +rule remain valid; no new protocol or blanket approval request is needed. The +remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof, +admission of the existing coding-client proposal with sanitized owner readiness +returns, then one attended negative/revoke/cleanup receipt committed with probe +clients removed. No native acceptance or credential/registration action is +inferred from this coordinator review. + + +## KeyCape issuer-source compatibility inspection for lifecycle v2 + +```task +id: KEY-WP-0039-T03 +status: done +priority: medium +state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73" +``` + +2026-10-11: Completed KeyCape's own issuer-source inspection for +KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256 +6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed. +Read AGENTS.md, governing INTENT.md, current client-credentials claim source, +registration fragments, native caller-authentication commands and frozen v2 +issuer transport/probe wiring. Seventeen unchanged runner tests pass locally. +This task completes only issuer-source inspection; the resulting narrow +source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment). +T01 stays done and T02 stays open for actual native registration, token input, +refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner +checkout was changed during this source inspection. Owner source files were +read for the bounded issuer compatibility check. + +Current token.go:306-309 returns HTTP 401 invalid_profile_usage with +feature=client_id for a removed confidential client; wrong-secret +feature=Authorization cannot count as removed-registration evidence. The +proposal and packet comments are narrowly corrected against that current +issuer contract. Prior T01/source re-agreement history is preserved; this source +acknowledgment supplies no NetKingdom runtime-owner review or founder live go. + +The private token-producer handoff remains a real execution gap under T02. +Existing keycape login/service-token commands require a new private output +file; login also needs a concrete admitted public PKCE client/callback and +attended browser/MFA. The lifecycle expects an in-memory private stdin object +containing human_token and coding_agent_token, but no reviewed file-to-process +or process-to-process producer, admitted human client/callback, custody-injected +coding-agent source, native placement or producer cleanup receipt has been +provided. No unattended token producer or admitted secret route is invented. +Resolve those exact owner contracts before founder native approval; existing +static registration is not live token custody or producer admission. + + +2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed +KeyCape issuer-facing inspection before the new formal acknowledgment in +docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4 +manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8, +executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d. +NetKingdom's independent V4 source review is approved, receipt SHA256 +4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb. +The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain +retained as one correction trial; they are not extra scored successes. + +V4 retains the unchanged issuer runner and six-client claim design, and fixes +finite/nonboolean windows, true total request deadlines, late-known OpenBao +session private revoke/refusal and guarded bounded final Secret deletion. +The issuer-facing acknowledgement covers only stopping late token exchanges, +claim source/shape and preserving token cleanup failure/unknown outcomes; it +does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41 +supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass +locally. No native tokens, credentials, issuer mutations or live cleanup were +performed. T01 stays done; T02 stays open and the genuine private token-producer +handoff remains its next source prerequisite before actual founder native go. + + +2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed +KeyCape's own current issuer-source/claim/cleanup-contract inspection before +any new formal acknowledgment. Current token.go:306-309 still distinguishes +removed-client invalid_profile_usage feature=client_id from wrong-secret +feature=Authorization. The six-client design, audience source and expiry +positive control remain unchanged, and the original runner's 17 tests pass. +Exact V5 manifest SHA256 +d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285; +executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360. +V5 has 46 passing supplied lifecycle checks. It adds a single exceptional +response-close abort under a fixed20ms cap clipped to nonresettable aggregate +cleanup, and to positive original request time remaining, with expired aggregate +zero calls, fixed private errors and truthful known-session revoke/unknown +outcomes. This inspection covers issuer-facing token lifetime and claim +interfaces only; NetKingdom and Platform independently own their runtime and +backend review decisions. Previous V2/V3 Net source refusals and Platform V4 +exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are +historical narrow source inspections, never aggregate native approval. +T01 remains done, T02 open. No native actions or credentials occurred. The real +private token-producer/client/callback/coding-agent custody/cleanup handoff gap +stays the next prerequisite; no unattended admission is inferred. + +Exact independent V5 source decisions were then verified: NetKingdom approved +receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc +(46 supplied +17 runner +21 independent checks); Platform approved receipt +SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0 +(46 supplied +5 independent +17 previously verified runner checks). Those are +foreign owners' own source decisions, not KeyCape runtime/backend reviews or +founder native authority. Formal KeyCape bounded issuer acknowledgment follows +in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json. + + +2026-10-11 T02 continuing source preparation: private producer decision packet +`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI +flows, selected existing netkingdom-bootstrap-console literal-loopback human +client subject to owner purpose/MFA/listener admission, and the existing +codex-railiance-platform source identity subject to actual live/custody/consumer +admission. No existing coding-agent delivery receipt was found; AppRole tokens +do not substitute for issuer JWTs. A bounded pipe-only adapter for the current +CLI's two caller-owned private files has 18 passing synthetic checks, with +identity-guarded explicit file consumption and fixed value-free failure output. +Decoded claim shape does not prove signatures or authenticated admission. +See docs/openbao-negative-token-producer-decision.md and .json for exact owner +choices and missing receipts. The exact trusted workstation-to-node launcher, +private-file allowance and railiance01 Bao route/public-health guard still need +source review and founder disposition before any token/temporary credential/ +mutation/restart. No registration, custody, login, native action or credential +request occurred. V5 and failed review histories retained; T01 stays done and +T02 open. No new work record or native acceptance is inferred. + + +2026-10-11 same T02 source-preparation correction for +KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer +refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink). +V2 has source-pinned private launch/ready handshake compatible with exact +Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before +ANY caller-token file load/consume/write; separate local data/control pipes +reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget +only clips downward after readiness. 31 distinct producer/launcher synthetic +checks and 74 compatible consumer supplied checks pass. No SSH/native/login or +credential access was performed. Stat/unlink remains non-atomic and requires +continuously exclusive sameUID custody; the retained race test is a custody +violation counterexample, not a claim of repaired filesystem atomicity. Receipt +keeps atomic delete/copy cleanup unverified and possible copies true. Exact +trusted launcher/directSSH profile/private tmpfs allowance, selected human +client/MFA purpose reuse and coding-agent registration/custody/delivery remain +unadmitted owner choices. Native node route, role/leeway/window/founder decision +and actual negative/cleanup acceptance remain pending. T01done and T02open; no +new task or plan, and V1/V5-V8/current failure histories are not overwritten. + + +2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair +source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty +now propagates into sticky remote consumer uncertainty before launcher early +return; local SSH termination never proves remote no-effects/recovery. Actual +full-payload then interrupted confirmation and4096-byte partialpipe failures +are retained regressions. 33 distinct source checks pass. Exact Net V9 source +bytes unchanged; new source reviews required, not implicit repin. Native +client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session +authority remain unadmitted. T01done/T02open; no new task/plan or native action. + + +2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal +NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded +nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation +budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not +a renewed launcher allowance. 40 own source tests plus9 retained paired +wrapper/child handshake regressions pass with synthetic/no native transport. +V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact +bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/ +maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/ +credentials/native/owner checkout changes; no task/plan duplication. T01done +and T02open; source correction is not native acceptance. + + +2026-10-11 T02 source-design disposition checkpoint: founder accepted exact +source memo `infd-20261011-key39-source-design-r1`, review +`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`. +Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval +`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject +`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL: +`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`. +Root's authenticated confirmed readback is +`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`, +SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed +`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256 +`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256 +`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction. + +Acceptance covers recommended source preparation for existing bootstrap-console +human-client purpose reuse, returning existing coding-agent custody admission or +preparing a narrow owner proposal, proposed founder directSSH source profile, +and conditional exclusive-owner tmpfs files. It does not establish actual +registration/MFA/listener, coding-agent custody/delivery, transport/file admission, +node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh +attended native go. The published choice documents remain historical exact +pending proposals; this appended receipt records their later source disposition. +T02 remains wait on external owner custody/admission and separate attended +execution authorization. Source acceptance permits further source preparation; +actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03 +remain done. No native or credential action is authorized by this source receipt. +See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact +owner returns and safe metadata evidence needed before preparing a runnable +attended execution proposal. + +2026-10-11 ordered-goal qualification (T02 only): the final source runner already +exists and its 17 local semantic tests pass. No new helper or source protocol is +needed to repeat accepted preparation. Native execution follows the first +Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03); +only those task metadata were checked, not wait-task bodies. NetKingdom owns the +registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns +leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is +already recorded resolved; do not ask again. Attended human authentication and +owner-held existing coding-agent custody go on the single attended checklist. +No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred. + +2026-10-17 Platform owner returns (source-prepared, not admitted): +railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md` +(commit `0a9617a`). The coding client `codex-railiance-platform` has no +admission, custody or delivery record. The existing narrow proposal +`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`) +remains the one to admit, so the coding-agent case stays unrunnable until owners +admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches +the packet's claim design. It declares no `expiration_leeway`, so the expiry +probe must assume OpenBao's 150s default until an attended sanitized read +confirms the live value. The node-local Bao origin is not declared in source and +also needs that attended read. T02 stays wait. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0040-mfa-attended-run-findings.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0040-mfa-attended-run-findings.md new file mode 100644 index 00000000..8e8730c8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0040-mfa-attended-run-findings.md @@ -0,0 +1,162 @@ +--- +id: KEY-WP-0040 +type: workplan +title: "Fix findings from the first attended MFA run (factor recovery preview, OTP prompt help)" +domain: infotech +repo: key-cape +status: finished +owner: claude-code +topic_slug: netkingdom +created: "2026-10-11" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +state_hub_workstream_id: "a67f9d60-9b8f-5111-9fe5-9f64e631b1c3" +--- + +From the attended run of 2026-10-11 (user-engine `docs/evidence/2026-10-11-mfa-attended-u05-u08.md`). +U05–U07 passed. U08 is blocked by T01. + +## Recovery preview rejects valid input (F6) + +```task +id: KEY-WP-0040-T01 +status: done +priority: high +state_hub_task_id: "b385602d-714a-5fdb-b40b-bd870420f28e" +``` + +Operator `platform-root` (holds platform-operator, fresh AAL2) previewed login `tegwick`, +reference `U08-2026-10-11` at `/platform/factor-recovery`. Every attempt returned `invalid_request` +("Enter a valid login and support reference."). Both inputs match `[A-Za-z0-9_.@:/-]{1,150}`. +The `factor-recovery` container in `mfa/privacyidea` logged nothing. + +Candidate causes in `scripts/recovery_service.py` / `scripts/factor_recovery.py`: +- the actor `sub` (KeyCape `user.ID`) failing `_recover`'s character rule; +- a privacyIDEA serial failing it; +- the request body differing from what is validated, e.g. form encoding through the portal. + +Done when: +- the failing check is identified and fixed with a regression test; +- the sidecar logs the failing check (field name and error code, never values); +- a preview for `tegwick` lists the active TOTP (serial suffix …E1D) without applying anything. + +Then hand back to user-engine, which re-runs U08 (USER-WP-0028-T03). The fix and roll-out +of the sidecar are blue lane under the build-mode change rule. + +2026-10-12 source repair: LLDAP adapter returns entry.DN as user.ID and the token +issuer emits that exact value as sub. Recovery incorrectly applied the login +regex to actor, rejecting comma/equal characters. Two DN-shaped subject tests +reproduce invalid_request against the old source. The repair validates the +bounded printable ASCII actor separately, preserves exact ticket/audit binding, +and keeps target/serial/realm/reference validation strict. Validation diagnostics +emit only closed field names and error codes to stderr; provider logging stays +disabled. Twenty recovery tests pass, including identity switching, malformed +input, provider field failures, replay and mutation/audit guards. Go OIDC and +LLDAP suites pass. No live factor was read or changed. T01 stays progress until +the deployed authenticated preview succeeds and user-engine can rerun U08. + +## Link the OTP prompt to lost-authenticator help (F3) + +```task +id: KEY-WP-0040-T02 +status: done +priority: medium +state_hub_task_id: "3ad155dd-15ef-5522-bdf9-c78832728553" +``` + +The OTP input offers no way out for a user whose code is rejected or whose device is lost. Add +"Code rejected or lost your authenticator?" linking to +`https://users.coulomb.social/security` (the lost-authenticator section). The same link serves users +who have no factor yet (the dead end noted in `mfa-attended-run.md`). + +2026-10-12: source link added to the MFA challenge with noreferrer, preserving +challenge state and requiring no query/token in the destination. Go OIDC tests +pass. Runtime publication and attended journey verification remain open; T02 +is not represented as deployed. + +2026-10-12 blue-lane runtime repair completed: immutable ConfigMap +factor-recovery-01119187d90d7b76 replaced only the recovery-code volume through +UID/resourceVersion CAS. Server dry-run proved no other pod-template change. +privacyIDEA generation 11 is Ready; recovery health returns 200 and anonymous +recovery is denied (403). All Go tests pass in the actual checkout. An early +ready=false observation was retained while the rollout completed; no apply was +replayed. Abort owner: key-cape; rollback restores prior immutable ConfigMap +factor-recovery-79e2964c112f3688 with the same CAS helper. Receipt and exact +proposal: the-custodian/docs/evidence/2026-10-12-netkingdom-security/. +T01 remains progress for the founder's authenticated tegwick preview and U08. +T02 is a local source edit only; its source/image publication remains pending. + +2026-10-14: the MFA challenge render test now requires the lost-authenticator +link after the OTP form, with rel="noreferrer" and no request state in the URL. +T02 stays progress until that page is published and an attended journey sees it. + +## Current publication and attended acceptance — 2026-10-16 + +The source-only publication statements above describe the October 12–14 state. +They are superseded for T02: the October 15 founder-run publication recorded in +`docs/operations.md` (Current image) and KEY-WP-0041-T02 delivered image +`sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010` +from source `8c6c071`. That source includes the OTP help link commit `205a1c0`. +The founder verified the image-only diff, server dry-run, Recreate rollout, +Ready pod with zero restarts and discovery HTTP200. No additional image rollout +is needed to make the existing T02 link available. This reconciliation makes +no new live observation and does not infer attended acceptance from publication. + +T01 still needs the actual authenticated preview; T02 still needs the actual +OTP-page observation. Both remain progress. Resume the existing owner procedure +`user-engine/docs/mfa-attended-run.md`, corrected by its October 11 evidence +`user-engine/docs/evidence/2026-10-11-mfa-attended-u05-u08.md`: + +1. In the real user-engine-portal OTP challenge, observe **Code rejected or lost + your authenticator?**, follow it to `https://users.coulomb.social/security`, + and confirm the rejected-code/lost-device guidance is reachable. Preserve no + challenge URL, request state, cookie or OTP value in evidence. Record time, + client, link text/destination and outcome. This supplies T02 acceptance. +2. With platform-root's own fresh AAL2 browser session, open the existing + `/platform/factor-recovery` form. Preview `tegwick` using the existing support + reference `U08-2026-10-11`; inspect any existing reference before changing it. + Confirm the current factor metadata and shared-identity warning. The October + 11 suffix was …E1D; a changed factor is a reason to reconcile current state, + never to force the old selection. Record only time, preview outcome and serial + suffix. This preview alone makes no factor change and supplies the remaining + T01 preview evidence. +3. Only in the separately attended recovery window, after the operator verifies + the person and exact current scope, continue the existing U08 confirmation, + audited disable, reference readback, replacement enrollment and fresh login + procedure. Preserve the original reference and safe recovery record id. + Neither the published bug fix nor a passing preview closes U08. Stop on + stale metadata, replaced factor or failed audit/readback; do not reset or + re-enroll merely to make an acceptance check pass. + +No authenticated preview, real-factor mutation or attended page observation was +performed during this source reconciliation. No passwords, codes, token values, +provider keys or production Secret objects were requested or read. + +## T02 attended acceptance — 2026-10-17 + +The founder reported in session that the real user-engine-portal OTP challenge +shows **Code rejected or lost your authenticator?**, that it leads to +`https://users.coulomb.social/security`, and that the lost-device guidance is +reachable there ("T02 is fine"). This is the founder's own observation, on the +deployed image `cdec9dd7` from source `8c6c071`. No exact time or client was +recorded, and no challenge URL, request state, cookie or OTP value was preserved. +T02 is done. T01 still waits for the authenticated preview (step 2 above). + +## T01 attended preview acceptance — 2026-10-17 + +The founder, signed in as platform-root with its own fresh OTP (AAL2), opened +`/platform/factor-recovery` and previewed login `tegwick` with the existing +reference `U08-2026-10-11`. The founder reported that the preview listed +tegwick's TOTP. The `invalid_request` failure from October 11 (F6) did not recur. +No confirm or disable action was taken, and no factor was changed. When asked +afterwards, the founder confirmed that the listed TOTP's serial suffix is …E1D, +the same active factor as on October 11. The exact time was not recorded. + +All three Done-when conditions now hold: the DN-shaped actor defect was fixed +with regression tests (2026-10-12); the sidecar logs only closed field names and +error codes; and the deployed preview for tegwick lists the active TOTP …E1D +without applying anything. That third condition was met only after the suffix +was confirmed, not before. T01 and the workplan are done. U08 itself, meaning +the attended disable, re-enrolment and fresh login, belongs to user-engine +(USER-WP-0028-T03) and is handed back to it. This workplan does not close U08. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0041-sign-in-diagnosis-page.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0041-sign-in-diagnosis-page.md new file mode 100644 index 00000000..56fd0581 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency-backup/workplans/KEY-WP-0041-sign-in-diagnosis-page.md @@ -0,0 +1,63 @@ +--- +id: KEY-WP-0041 +type: workplan +title: "Sign-in diagnosis page and auth log uid (NK-WP-0051-T02)" +domain: infotech +repo: key-cape +status: finished +owner: claude-code +topic_slug: netkingdom +created: "2026-10-14" +updated: "2026-10-15" +flavor: implementation +depends_on: [] +state_hub_workstream_id: "092c7590-3700-524d-bbe0-a934e063be6b" +--- + +Demand: NetKingdom `NK-WP-0051-T02`. The field names (`uid`, `groups`, +`assurance`, `client_id`) and the never-show/never-log list come only from +`net-kingdom/docs/sign-in-diagnosis.md`; this workplan does not restate them. + +## Opt-in pre-redirect page and auth log fields + +```task +id: KEY-WP-0041-T01 +status: done +priority: high +state_hub_task_id: "5947be07-bc84-5672-b807-608070a39d10" +``` + +- Client registration flag `signInDiagnosis` (default false) in the YAML + config, `domain.Client` and `spec/canonical-model.yaml`. +- With the flag on, `/authorize/callback` renders a page before the redirect + with `uid`, sorted `groups`, `assurance` and `client_id`, one continue link + to the unchanged redirect (code + state), and a sign-out hint. html/template + escaping, `no-store`, `Referrer-Policy: no-referrer`, restrictive CSP. +- `assurance` is `aal2` only when this request verified a one-time code; a + reused aal2 session is `aal1` (contract wording, stricter than the session + satisfied `MFAVerified`). +- The `auth_success` log line gains `uid` and `assurance` beside `client_id`. +- Enabled for `openbao-admin` in `config/dev-config.yaml`; the live flag is set + in `sso/keycape-config` (see `docs/operations.md`). +- Tests: `src/internal/server/oidc/signin_diagnosis_test.go`. + +## Deploy the image and record its digest + +```task +id: KEY-WP-0041-T02 +status: done +priority: high +state_hub_task_id: "bd5db26c-b71c-5291-bdc3-7c0f718651db" +``` + +Build and push with `make push IMAGE_TAG=`, set `signInDiagnosis: true` +on the OpenBao browser client in `sso/keycape-config`, roll the issuer under +the usual live-change approval, and send the digest to net-kingdom for the +evidence table in `docs/sign-in-diagnosis.md`. Not done by the implementing +session: deployment needs operator approval. + +2026-10-15: image `sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010` +(source `8c6c071`) is live; see `docs/operations.md`. 2026-10-15: `signInDiagnosis: true` set on `openbao-admin` with +net-kingdom `openbao-signin-diagnosis-rollout.py` (rv 71655453 → 75372162, +unrelated bytes preserved) and the issuer restarted; pod ready, discovery 200. +Accepted 2026-10-15: an attended OpenBao login showed the page and completed. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stderr.txt new file mode 100644 index 00000000..3345563f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stderr.txt @@ -0,0 +1 @@ +WARN: git commit failed for writeback: Generated-only commits rejected: staged diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stdout.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stdout.json new file mode 100644 index 00000000..597a0899 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/consistency.stdout.json @@ -0,0 +1,123 @@ +{ + "repo_slug": "key-cape", + "repo_path": "/home/worsch/key-cape", + "source_revision": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-24", + "message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-11", + "message": "Task 'ADHOC-2026-09-27-T02' has no state_hub_task_id", + "file_path": "workplans/ADHOC-2026-09-27.md#ADHOC-2026-09-27-T02", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub", + "file_path": "workplans/KEY-WP-0007-user-engine-portal-oidc-client.md", + "db_id": "", + "file_value": "USER-WP-0020", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'NK-WP-0025' is not linked to State Hub", + "file_path": "workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md", + "db_id": "", + "file_value": "NK-WP-0025", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'NK-WP-0030' is not linked to State Hub", + "file_path": "workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md", + "db_id": "", + "file_value": "NK-WP-0030", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0027' is not linked to State Hub", + "file_path": "workplans/KEY-WP-0010-openbao-operator-loopback-callback.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0027", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'KEY-WP-0039-T02': file content differs from DB (file wins)", + "file_path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md#KEY-WP-0039-T02", + "db_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "file_value": "2026-10-11: requalified \u2014 live registration go caafe4be was already resolved; owner authorization present. See the-custo", + "db_value": "2026-10-11: requalified \u2014 live registration go caafe4be was already resolved; owner authorization present. See the-custo", + "fixable": true + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'keycape-implementation@retired-20260826' (id=2c9caad8\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "2c9caad8-2ced-492d-9d63-376387b4b9b0", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "2 unread inbox message(s) older than 3 day(s): the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "2 unread message(s) lack thread_id for supersession tracking: the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 44/44", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-11 skipped: task 'ADHOC-2026-09-27-T02' in finished workstream \u2014 not created", + "C-22 fixed: task 03da4c38\u2026 description updated", + "brief: .custodian-brief.md updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 9, + "info": 1 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/final-verification.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/final-verification.json new file mode 100644 index 00000000..e2cafe7c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/final-verification.json @@ -0,0 +1,46 @@ +{ + "commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "receiver_status": "applied", + "acknowledge_retirements": false, + "instance_role": "primary", + "instance_label": "railiance01", + "workplan": { + "id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "status": "active", + "record_id": "KEY-WP-0039" + }, + "task_bindings": [ + { + "record_id": "KEY-WP-0039-T01", + "state_hub_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8", + "status": "done", + "workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "depends_on_source": [] + }, + { + "record_id": "KEY-WP-0039-T02", + "state_hub_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "status": "wait", + "workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "depends_on_source": [ + "KEY-WP-0039-T01", + "KEY-WP-0038-T02", + "KEY-WP-0038-T03" + ] + }, + { + "record_id": "KEY-WP-0039-T03", + "state_hub_task_id": "54d37ad8-2352-51c2-bb2e-af6070dbbd73", + "status": "done", + "workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "depends_on_source": [] + } + ], + "all_three_source_blocks_unchanged": true, + "frontmatter_unchanged": true, + "only_committed_path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md", + "remote_forge_matches": true, + "owner_worktree_and_index_clean": true, + "normal_sync_refusal": "unrelated grandfathered ADHOC-2026-09-27-T02 UUID; not modified", + "native_acceptance_inferred": false +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/manifest.json new file mode 100644 index 00000000..281f33c7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/manifest.json @@ -0,0 +1,69 @@ +{ + "scope": "one-path source narrative publication and metadata-only projection; no native or credential action", + "commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "files": { + "KEY39.preimage.md": "sha256:7b4b97f9a9e6b8291078e187b967a5356702dd7817880f92c12649d764e5e0ae", + "KEY39.proposed.md": "sha256:c576db4b97ea5de0a50e17f251f8d5c57a744ded557bede963e48608bf3a6ca2", + "apply.py": "sha256:b5da81d5ae906dd870df7fbbda624d9fc1e5851d6630704a095015f33a91047e", + "baseline.json": "sha256:17e56f0f3a4560f1c679b46ee61496ca6568ce79b4a378e9d5bba98ec949ef1c", + "canonical-receiver.json": "sha256:75f88389f93f00f2dea43cd8eca9e4b18e443ad2c313c4e3d4b9f2c95701e818", + "canonical-reconcile.py": "sha256:eb62e75e8ef2a98a5eef7f5975e164f07885c7c56e205a2cf34a6618f48e5753", + "consistency-backup/.custodian-brief.md": "sha256:96d84539b3a92786bff37fcc70161adec4648b9547287e52e6b817bc080a080a", + "consistency-backup/WORK-RECORDS.md": "sha256:431842547bd82750f284f8c1c9f519892f1bb1c1a684b9b1e38adc6fdfdc299d", + "consistency-backup/originals.json": "sha256:a1580d2fae1ba3e4b3f344b37632c2f70ae7d1d40014fccb1ef6f8c0ca507665", + "consistency-backup/workplans/ADHOC-2026-09-05.md": "sha256:76123d22d0c1efc51212f8914e386f078b3c6d908959af6978ae73874e492b04", + "consistency-backup/workplans/ADHOC-2026-09-07.md": "sha256:c335ebfe91dad291277e8f17034ac8f785b3bc767f281f205c446c08f03d7978", + "consistency-backup/workplans/ADHOC-2026-09-27.md": "sha256:2794d1940fc3c5e4a2df57de9d4b742fa6aa8602b67f1d3177c0d96bc5aaf314", + "consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md": "sha256:29a2bf4892b7185e28e9126a3d8311d0fed34a876f416a3e75c605915fde0969", + "consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md": "sha256:76fee187185b30219bc257ccc9b587661df4489574a2bb57703a365a5d803cec", + "consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md": "sha256:5e949c696e587540d305b682eb1f8390ec544bbec6a566be8d04e73fb1b254ca", + "consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md": "sha256:260ba7d3116db3bc5a72e5cd8b9984137e7e372c8128d21073614dbd948c10c2", + "consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md": "sha256:c6702b644cdd046c0d3bba8ba8837050826e4dc3d855bd73327ae94218ba5b57", + "consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md": "sha256:b3227849978c95cad89e02afb74bf2217b6188ae65c5b0af6a21703d65477701", + "consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md": "sha256:5408ff6dfeb58a8a7756b48fe4f54f7c7b98d7940cb9aaa5c7864f1247e58150", + "consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md": "sha256:bfc9b2118f0712e92a86bbb0f14a2857fafc3a42a3c14c0e2535002aaa57a2f5", + "consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md": "sha256:d1daac9815930104a392ea96c14922e34f947720a31d1b0ac45a770997e0f476", + "consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md": "sha256:41d28a16bef9313b776c5c6305316a9e7d4f5528b7fb7427b8384a0ed0ad35c4", + "consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md": "sha256:3c5458180fe81a04e357f8db737e4287f79640b5ab02682bcadc595b73f846cd", + "consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md": "sha256:68d34d75c914e7d2d8177000e800d6372cccda2b850c03ab85da42b90a66b91c", + "consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md": "sha256:55c88bf4ed9a456db59b319bbaf960dfe6f4147437e339f950e0583d7582057a", + "consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md": "sha256:22aac43ae5039aebd11f9f0aa57a882ad009ad6ed119ec08eb85b32a5af48cd7", + "consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md": "sha256:60b9f2265eff880d916c3ee8dba36aec9984ec310804de776cebf89cc98fdb95", + "consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md": "sha256:ed48128a1cb51cbe3d154727bafd5a5ffb3b5b618c20649b2a0e7db154cf3a0c", + "consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md": "sha256:8700e0e5aa07dcd7f651e792cca12a0330b7ee9d2f6064b4cc3fcf4c7d509340", + "consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md": "sha256:15026b708d116f803b0b2fa43f30a4dbced76e4d8137b5fcb5b4275e593d7c7b", + "consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md": "sha256:0d2d8b5bda1e0d333935812d8d3749cb7b0c5523a3987d1b26185f9f6d0f0fa2", + "consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md": "sha256:cc842d15bf42ddc4d0f4056bc19b3176cab2c5f3c1a2fe6d648e5dbd077be8b3", + "consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md": "sha256:b603248693073a9b9b25b4d0a4147d8d88c16a07dc9936e48fa1f3c76fe8bab6", + "consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md": "sha256:8c135e84be81de31b4949f52ac28dccd404f2e4c3eaa576d733bd50160427a53", + "consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md": "sha256:e581cd11f3905805b5e8ba8e2ede85092577c7d086e1761f2a2d033fdead4e47", + "consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md": "sha256:af7842fa97263bfd46a4b5ae84c5c1ecddb06ab9623586e0292dcc93099fea35", + "consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md": "sha256:0addb34e6d85e376c6a5db7afba597b6f268afa98b86c8c60add9be174dd1b26", + "consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md": "sha256:c1bc0de47511ae49102009a172c84d9d5d64e829a18c8d40564fe817935563b3", + "consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md": "sha256:f559eae61d433d451f80ea575254bb14a47f2485267b412e42d524b434bb1b59", + "consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md": "sha256:fbd51422fa5e6449000e4520e6639f6135c7393165a5ae02b1e92892edba0480", + "consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md": "sha256:3241f3b94db9cad5e3770cf1fedc5c211288357e4f69b5ba9da57065094bdf0e", + "consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md": "sha256:c94c3afc7fb4270dfc18f3fe8a32946c524e4a6ba2d02a3d8fda69c9b8441af0", + "consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md": "sha256:0abb135367f0366aa071769c4bd76fdeb321d810ec75ad3b58d718734f78382b", + "consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md": "sha256:6a5cd483a40b9477227b02156c42b3469094dac145b33727b336118898aa782a", + "consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md": "sha256:c01bb51cf2dcad06ea846040dddef09845041fa7271cd7015e5a34a49e60bb8f", + "consistency-backup/workplans/KEY-WP-0034-account-recovery.md": "sha256:7db9adcd418ccabfbce672aec283249a6e374212492f0349e8c47a568f130eda", + "consistency-backup/workplans/KEY-WP-0035-optional-mfa.md": "sha256:c60ea8d0c195686c0d2317f657aad2de27c9b9432d311627c0e1d06124cabb49", + "consistency-backup/workplans/KEY-WP-0036-platform-factor-recovery.md": "sha256:5ceb08fe5789b4f02550bb04fcaf8eeb25a43ac3f506dfc62d60a513bcd0013f", + "consistency-backup/workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md": "sha256:3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a", + "consistency-backup/workplans/KEY-WP-0038-forgejo-oidc-client.md": "sha256:dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064", + "consistency-backup/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md": "sha256:c576db4b97ea5de0a50e17f251f8d5c57a744ded557bede963e48608bf3a6ca2", + "consistency-backup/workplans/KEY-WP-0040-mfa-attended-run-findings.md": "sha256:98b23d8810bff047067169232213cb313ecc14ff79751224842723afe808fbd2", + "consistency-backup/workplans/KEY-WP-0041-sign-in-diagnosis-page.md": "sha256:7e44f46acd6b3942c1852b49e0d64b205dfd7d9bb378aed5241b1bf22d7ce88a", + "consistency.stderr.txt": "sha256:f4a6aa1609a13600a0b97cbb08bbc03b3e247b619ff37912d37f7e109ce798ba", + "consistency.stdout.json": "sha256:4e57d07949cb303ad144dd931da53efd4487245fbfc8baaa7e08e26dd7cfcb79", + "final-verification.json": "sha256:79e75662ed4ef0cca9b6dbc7b32c09f915189a7efc091d34220793e3fc185718", + "normal-publication.json": "sha256:a5b0e661613eaa4d2195256dd05ce7b61e2aca4d6c358552ab130255ecfef799", + "normal-sync.stderr.txt": "sha256:832bd22063c46072d0d91381f7d1356be933b4d09ded52fa19282da13c2855b1", + "normal-sync.stdout.json": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "one-path.patch": "sha256:da42eebb465eeb84fe74177475762837f0489d4c634ffcbadd3d502e4e9c3dc6", + "progress-receipt.json": "sha256:668c3950ccec8fd6c9830a23063828b8ffcf9170f3fbff1bb60b4f4553788467", + "progress-request.json": "sha256:3475285a3441fafc6c7d022397d1a553485e8f863cb609e70012731268e9d827", + "sync.py": "sha256:e33da990a19b207929a6aafc9d76aa646e79a27793cb1a2ff86c19f4f7dbc72b" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-publication.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-publication.json new file mode 100644 index 00000000..94bd1a6c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-publication.json @@ -0,0 +1,34 @@ +{ + "published_candidate_commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "normal_commands": [ + { + "command": [ + "statehub", + "fix-consistency", + "--repo", + "key-cape", + "--json" + ], + "exit_code": 0, + "label": "consistency" + }, + { + "command": [ + "/home/worsch/repo-manager/.venv/bin/rmgr", + "sync", + "--path", + "/home/worsch/key-cape", + "--push" + ], + "exit_code": 1, + "label": "normal-sync" + } + ], + "unrelated_writeback_restored": [ + ".custodian-brief.md" + ], + "all_original_index_worktree_fields_preserved": true, + "one_path_commit": true, + "normal_hooks": "changed-staged-blobs scanner8.30.1 clean", + "native_or_credential_actions": 0 +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-sync.stderr.txt new file mode 100644 index 00000000..682f290e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-sync.stderr.txt @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "/home/worsch/repo-manager/.venv/bin/rmgr", line 8, in + sys.exit(main()) + ^^^^^^ + File "/home/worsch/repo-manager/src/repo_manager/cli.py", line 853, in main + result = sync_repository_projection( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/worsch/repo-manager/src/repo_manager/projection_sync.py", line 98, in sync_repository_projection + assignments = ensure_missing_work_record_identifiers(repo_root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/worsch/repo-manager/src/repo_manager/identifiers.py", line 962, in ensure_missing_work_record_identifiers + raise ValueError( +ValueError: missing UUIDs on grandfathered or noncanonical records require an explicit identity decision: [{'kind': 'task', 'record_id': 'ADHOC-2026-09-27-T02', 'path': 'workplans/ADHOC-2026-09-27.md'}] diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-sync.stdout.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/normal-sync.stdout.json new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/one-path.patch b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/one-path.patch new file mode 100644 index 00000000..ca58a935 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/one-path.patch @@ -0,0 +1,24 @@ +diff --git a/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md b/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md +index f51f2e6..fd0fc46 100644 +--- a/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md ++++ b/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md +@@ -139,6 +139,19 @@ operator/ESO capability and nonmember checks remain independent acceptance. + 2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans. + + ++2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner ++acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md) ++confirms all 14 executable/test/producer-decision pins and three Platform source ++pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing ++source acceptance, resolved registration go caafe4be and the standing attended ++rule remain valid; no new protocol or blanket approval request is needed. The ++remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof, ++admission of the existing coding-client proposal with sanitized owner readiness ++returns, then one attended negative/revoke/cleanup receipt committed with probe ++clients removed. No native acceptance or credential/registration action is ++inferred from this coordinator review. ++ ++ + ## KeyCape issuer-source compatibility inspection for lifecycle v2 + + ```task