diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-receipt.json new file mode 100644 index 00000000..6218b2ce --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "id": "165e29df-5a58-4314-9ec7-f8420eff72f1", + "topic_id": null, + "task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "decision_id": null, + "event_type": "note", + "summary": "Published one KEY-WP-0039 T02 Custodian coordinator finalization-review paragraph in exact owner commit40de616e078906e04c692edf7cb7a8bfbc07b5ea. All3taskblocks/frontmatter/UUIDs/dependencies unchanged: T01done,T02wait,T03done; parentactive. Existing source acceptance/registration go/standing attended rule preserved; existing KEY38 then coding-client admission/readiness then attended negative/revoke/cleanup sequence retained, no native acceptance inferred. Required consistency passed; restored generated brief writeback. Normal rmgr sync refused unrelated grandfathered ADHOC-2026-09-27-T02 UUID; no historical identity mutation. Canonical primary railiance01 receiver applied exact published commit with acknowledge_retirements=false; forge, task bindings and clean owner index/worktree independently verified. No credential/native/registration action. Evidence docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-17T11:32:33.188005Z", + "workstream_id": "09c3bddf-6058-5603-8488-cccdb7be4f86" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-request.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-request.json new file mode 100644 index 00000000..2368e98e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/progress-request.json @@ -0,0 +1,7 @@ +{ + "summary": "Published one KEY-WP-0039 T02 Custodian coordinator finalization-review paragraph in exact owner commit40de616e078906e04c692edf7cb7a8bfbc07b5ea. All3taskblocks/frontmatter/UUIDs/dependencies unchanged: T01done,T02wait,T03done; parentactive. Existing source acceptance/registration go/standing attended rule preserved; existing KEY38 then coding-client admission/readiness then attended negative/revoke/cleanup sequence retained, no native acceptance inferred. Required consistency passed; restored generated brief writeback. Normal rmgr sync refused unrelated grandfathered ADHOC-2026-09-27-T02 UUID; no historical identity mutation. Canonical primary railiance01 receiver applied exact published commit with acknowledge_retirements=false; forge, task bindings and clean owner index/worktree independently verified. No credential/native/registration action. Evidence docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/.", + "event_type": "note", + "author": "codex", + "workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/sync.py b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/sync.py new file mode 100644 index 00000000..fd62ff1c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/sync.py @@ -0,0 +1,36 @@ +from pathlib import Path +import subprocess,json,hashlib,stat +root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1');backup=out/'consistency-backup';backup.mkdir() +def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode() +head=git('rev-parse','HEAD').strip();assert head.startswith('40de616') +assert git('diff-tree','--no-commit-id','--name-only','-r',head).splitlines()==['workplans/KEY-WP-0039-openbao-negative-probe-tokens.md'] +assert not git('status','--porcelain=v1') +paths=sorted({p.relative_to(root).as_posix() for p in (root/'workplans').rglob('*.md')}|{n for n in ('.custodian-brief.md','WORK-RECORDS.md') if (root/n).exists()}) +original={} +for rel in paths: + p=root/rel;assert p.is_file() and not p.is_symlink() + dest=backup/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(p.read_bytes()) + original[rel]={'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'mode':stat.S_IMODE(p.stat().st_mode),'index':git('ls-files','--stage','--',rel)} +(backup/'originals.json').write_text(json.dumps(original,indent=2)+'\n') +results=[];changed=[] +try: + for label,argv in [('consistency',['statehub','fix-consistency','--repo','key-cape','--json']),('normal-sync',['/home/worsch/repo-manager/.venv/bin/rmgr','sync','--path',str(root),'--push'])]: + cp=subprocess.run(argv,cwd=root,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=240) + (out/(label+'.stdout.json')).write_bytes(cp.stdout);(out/(label+'.stderr.txt')).write_bytes(cp.stderr) + results.append({'command':argv,'exit_code':cp.returncode,'label':label}) +finally: + assert git('rev-parse','HEAD').strip()==head,'unexpected commit; stop for review' + for rel,b in original.items(): + p=root/rel + if not p.exists() or hashlib.sha256(p.read_bytes()).hexdigest()!=b['sha256']: + changed.append(rel);p.write_bytes((backup/rel).read_bytes());p.chmod(b['mode']) + current=git('ls-files','--stage','--',rel) + if current!=b['index']: + assert b['index'];mode,oid,stage,path=b['index'].split();assert stage=='0' + git('update-index','--cacheinfo',mode,oid,path) + assert git('ls-files','--stage','--',rel)==b['index'] + assert hashlib.sha256(p.read_bytes()).hexdigest()==b['sha256'] + assert not git('status','--porcelain=v1'),git('status','--porcelain=v1') + receipt={'published_candidate_commit':head,'normal_commands':results,'unrelated_writeback_restored':changed,'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} + (out/'normal-publication.json').write_text(json.dumps(receipt,indent=2)+'\n') + print(json.dumps(receipt)) diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/KEY39.preimage.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/KEY39.preimage.md new file mode 100644 index 00000000..fd0fc46e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/KEY39.preimage.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-scope-clarification-r1/KEY39.proposed.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/KEY39.proposed.md new file mode 100644 index 00000000..b22aa6a1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/KEY39.proposed.md @@ -0,0 +1,368 @@ +--- +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 remains valid. Resolved registration go caafe4be covers only +the original secrets-engine-openbao client (KEY-WP-0037-T03, deployed 6f79a2af, +NK-WP-0046 exact-match helper); it does not authorize new probe clients or +coding-client custody, and no renewed original-client approval is requested. +The standing attended rule remains separate, for reviewed commands within +existing scope; no new protocol or blanket approval request is introduced. 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-scope-clarification-r1/apply.py b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/apply.py new file mode 100644 index 00000000..2783585e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/apply.py @@ -0,0 +1,23 @@ +from pathlib import Path +import subprocess,json,hashlib,re,yaml,difflib +root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1');out.mkdir() +rel='workplans/KEY-WP-0039-openbao-negative-probe-tokens.md';p=root/rel + +def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode() +base=git('rev-parse','HEAD').strip();assert base=='40de616e078906e04c692edf7cb7a8bfbc07b5ea';assert not git('status','--porcelain=v1') +before=p.read_text();old='''source acceptance, resolved registration go caafe4be and the standing attended +rule remain valid; no new protocol or blanket approval request is needed. The +''';new='''source acceptance remains valid. Resolved registration go caafe4be covers only +the original secrets-engine-openbao client (KEY-WP-0037-T03, deployed 6f79a2af, +NK-WP-0046 exact-match helper); it does not authorize new probe clients or +coding-client custody, and no renewed original-client approval is requested. +The standing attended rule remains separate, for reviewed commands within +existing scope; no new protocol or blanket approval request is introduced. The +''';assert before.count(old)==1;after=before.replace(old,new) +blocks=lambda text:re.findall(r'^```task\n.*?^```',text,re.M|re.S) +assert blocks(before)==blocks(after);assert before.split('---',2)[1]==after.split('---',2)[1] +(out/'KEY39.preimage.md').write_text(before);(out/'KEY39.proposed.md').write_text(after) +readback=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/registration-approval-readback.json') +baseline={'base_commit':base,'branch':git('branch','--show-current').strip(),'status':git('status','--porcelain=v1'),'index':git('ls-files','--stage'),'path':rel,'before_sha256':hashlib.sha256(before.encode()).hexdigest(),'after_sha256':hashlib.sha256(after.encode()).hexdigest(),'frontmatter':yaml.safe_load(before.split('---',2)[1]),'task_blocks':[yaml.safe_load(t.split('\n',1)[1].rsplit('```',1)[0]) for t in blocks(before)],'approval_scope_source':{'path':str(readback),'sha256':hashlib.sha256(readback.read_bytes()).hexdigest()},'assessment':'Clarification needed because previous coordinator wording grouped original-client go with standing rule without naming its object; no historical approval revoked or expanded.'};(out/'baseline.json').write_text(json.dumps(baseline,indent=2)+'\n') +p.write_text(after);(out/'one-path.patch').write_text(git('diff','--',rel));assert git('diff','--name-only').splitlines()==[rel] +print(json.dumps({'status':'current paragraph scope clarified','frontmatter_and_three_blocks_unchanged':True,'base':base})) diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/baseline.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/baseline.json new file mode 100644 index 00000000..33a69778 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/baseline.json @@ -0,0 +1,59 @@ +{ + "base_commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "branch": "main", + "status": "", + "index": "100644 0e8a5d95b6d263fbaeae4261a7051112fb27ee18 0\t.claude/rules/agents.md\n100644 7c2a64580a2fb55d6586e154ee70d801430faa3e 0\t.claude/rules/architecture.md\n100644 2ab2aee3c221b4c2f637786f66de431236b61bc5 0\t.claude/rules/credential-routing.md\n100644 5b6b1876a29a4de2c3e6fd0f67c3ffbeb144c496 0\t.claude/rules/first-session.md\n100644 b74d916907b49e9abed8a8bd8f4f3cd7aadf95ef 0\t.claude/rules/repo-boundary.md\n100644 c4b8fa16164b5a8b37fee603997121a5c9680aac 0\t.claude/rules/repo-identity.md\n100644 0c2d89e25d9b55e90982f7596f7336328a49c786 0\t.claude/rules/session-protocol.md\n100644 dc53ac69288ec906421a0e8e1e1083528c047702 0\t.claude/rules/stack-and-commands.md\n100644 f43cfff94d75b083b3ca10e7982654d71e75dfdf 0\t.claude/rules/workplan-convention.md\n100644 d3b36c3bffbeb922769a2a788e35b4113fccaa3f 0\t.custodian-brief.md\n100644 b1bcdc2c729352cada7b654b59a07fe6850fc466 0\t.forgejo/workflows/acceptance.yaml\n100644 0e16cc2ea75d37e1da788ca847c127fc04f5a98f 0\t.forgejo/workflows/image.yaml\n100644 e8491b77aad0b288977d3fb5eb8ca8db4a7725de 0\t.gitea/workflows/image.yaml\n100644 32a386e9115c36d1689d9ff595eab8d66f037286 0\t.github/workflows/ci.yml\n100644 e705f7f1738a49dd9a78b5907d68d9429107c67c 0\t.gitignore\n100644 4fc521fc9c4263b37c6978ea68651d1cb0c25faf 0\t.repo-classification.yaml\n100644 4a0df33e6635969b7158daec3e80bd28e5f0bbc1 0\tAGENTS.md\n100644 e81c1b48c16dc3bf49af30e48b476107d916ee21 0\tCLAUDE.md\n100644 6adf9ce0636d7782fd231e418a0009deeee9868b 0\tDockerfile\n100644 b2519df87f5b29b324187f97f7a0d92dff3fc874 0\tINTENT.md\n100644 7e08e28d5f32bdea3a9e1c828ae9f1e21d9edae7 0\tLICENSE\n100644 512fa11a68e460d410deff823e58b9608a342932 0\tMakefile\n100644 5435b16714d66247db4d99ab7782cf8cd21532d9 0\tREADME.md\n100644 ab2522b2e0c0e6c008810e9929fc0b8d35121512 0\tSCOPE.md\n100644 d86b29bbc480acfa6d12d5052039578d7d4e380c 0\tWORK-RECORDS.md\n100755 e7887dfbae490351f88d1f249e928f695f6a16d2 0\tbin/keycape\n100755 592504fd11f4c4fc821f3d6b0350b1285c38b0b0 0\tbin/keycape-to-keycloak\n100755 e49fe216aa04525d611c99ddf930639f98929ad0 0\tbin/lldap-export\n100755 9609697bd8e21396add3009893fcd8a11352eabd 0\tbin/lldap-to-ldap\n100755 b732e3274c05371e62768da3c3bdc3ac767329a0 0\tbin/validator\n100644 b43901fe8a7c3ca61f509b5cb2eac8b2ef091064 0\tcapabilities/playbooks/key-cape.lightweight-sso.yaml\n100644 2872ef7727ac0fc2c3943296098fbea6f2a85f62 0\tcapabilities/playbooks/key-cape.privacyidea-token-authority.yaml\n100644 bf8ea915b24d93be803ed1ef8c18c13a82976c9b 0\tconfig/dev-config.yaml\n100644 a4b13e5cffdc1cb683a202b90724bd86f565ccda 0\tconfig/service-clients.example.yaml\n100644 fa050d2a3722fe7372938ec2cb34b31244b7aa61 0\tdocker-compose.dev.yml\n100644 a8976c28eb6c40af92e1daadcf168aaef8647ad5 0\tdocker-compose.scenario-b.yml\n100644 5743fa39140e5bac1f2cad2479c5928470e76e1c 0\tdocker-compose.scenario-c.yml\n100644 52ab3e94c61c02ea0b13a0b8cf4e3f7d0f89eb40 0\tdocs/adr/ADR-0001-choose-go-for-keycape.md\n100644 f42bfba2462bbc739f409bb817ab85904236e3d1 0\tdocs/approval-clients-deployment.patch.yaml\n100644 08b3e12ea62dd0da1354c59060434b88c9e7040f 0\tdocs/approval-clients-rollout.md\n100644 3c35fe6242d82b72a0a66b3da17f8e5dd078df2e 0\tdocs/approval-engine-auth-contract.md\n100644 902c20cb3cfa6d4592947ac76df88a2fa5186649 0\tdocs/approval-engine-provisioning-request.yaml\n100644 f305058adf60bef56762c2588b5b5b2474f45430 0\tdocs/authorization-code-bindings.md\n100644 de742b60f5872ad84195f33f2a5763952653fcd5 0\tdocs/credential-renewal-release-2026-09-13.md\n100644 c2061c6a760e810adccdc404e044cb79baa4fdcb 0\tdocs/enrollment-assurance-release-2026-09-13.md\n100644 85dab93ed1f2196810ead4f6b81a897fd06c33ff 0\tdocs/evidence/2026-09-08-upstream-issuer-live-proof.json\n100644 6ae6fc721df48426e93e505206a0df8e18ef2499 0\tdocs/evidence/2026-09-09-keycape-verifier-admission.json\n100644 62fe341c90c514a92585641d14287f60076e8836 0\tdocs/evidence/2026-09-09-upstream-issuer-pin.json\n100644 cc87339baeae5406bd7ce76fe89bd0aaf95b366f 0\tdocs/evidence/2026-09-14-informed-decision-browser-registration.json\n100644 200863c7e6b87f3045c1bffd2b951be1bdee5d84 0\tdocs/evidence/2026-09-24-fresh-login-and-account-switch.md\n100644 2278d9917b56923887b64b157b10de537739ee55 0\tdocs/evidence/2026-10-07-secrets-engine-openbao-issuance.json\n100644 d97906b5ee836fca2e52bbe42a462b02200f4fe5 0\tdocs/evidence/2026-10-11-key39-next-source-inspection.json\n100644 88d98b835b63a016ede535cdf6416add36c17514 0\tdocs/evidence/upstream-issuer-probe.json\n100644 04d0bccc32fa1e0d5739f22b5d71a638e345e386 0\tdocs/forgejo-oidc-client-request.yaml\n100644 e714df21c4113ac853f2fbbf9309c06ea1b05863 0\tdocs/native-authentication.md\n100644 9f3c00bf7dcef2b3fa40a1c0fd99a7c03cb8facf 0\tdocs/offline-checker-owner-review-2026-10-16.json\n100644 be834c5854e2d1ac3f2747fb45dd80953508ff45 0\tdocs/offline-checker-owner-review-2026-10-16.md\n100644 259f7e10fb0a6aaa071aac342906cde85c056609 0\tdocs/oidc-browser-relying-party-type-review.md\n100644 713036618418c02f612071f41c4864b9b67c900a 0\tdocs/openbao-negative-probe-clients.yaml\n100644 fe9a9945d837fec98dc15452c1e7935b6a4fd0f1 0\tdocs/openbao-negative-probe-session-proposal.md\n100644 d3b806af1dd024bfc0f518d30ef26e1b715be361 0\tdocs/openbao-negative-token-producer-decision.json\n100644 e44cbcb98a7bb4efd727a026e70db45f5195fa30 0\tdocs/openbao-negative-token-producer-decision.md\n100644 3416a5943e1b535870d16841c10a89fdb65521c8 0\tdocs/openbao-service-auth-contract.md\n100644 3092a16568b37610407e4d613b01a18bead5e960 0\tdocs/operations.md\n100644 f95f57aaa5cfd6203ea5d1b0ce99369ec32c40cc 0\tdocs/optional-mfa.md\n100644 2be7eb82c4ee1ec9a4b9227d2163b063063e40f7 0\tdocs/proposals/2026-10-11-key39-founder-source-design-decision.json\n100644 ad23cbde3d063f40e85948d0b37ec99847f24840 0\tdocs/proposals/2026-10-11-key39-founder-source-design-decision.md\n100644 09fe2e81cdda1372fc0ed7ec2e3103d303d7211d 0\tdocs/proposals/2026-10-11-key39-next-execution-prerequisites.json\n100644 714a84c4a5d9e9258110b703297763537bbd07ec 0\tdocs/qonto-runtime-identity-contract.md\n100644 315a7210651a2777eeca2a5f502cfa5b91207600 0\tdocs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json\n100644 6c462037bb51a833451f37ddcca6a347656f354d 0\tdocs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.sources.json\n100644 af94cc71214d638d2e33cdb455b29c9ef5cfb1f4 0\tdocs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json\n100644 1a1ebdddca7e3e1c419ef293466174644dfdec1b 0\tdocs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.sources.json\n100644 9ad2ee43cf5c66a66ff0472f4c9f40d0e550283a 0\tdocs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json\n100644 567ee0f3aa1b14f8633e874a35530eb76b8c93c4 0\tdocs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.sources.json\n100644 f0aa85ec5484751c8e490e2c60a77a600e21a1e1 0\tdocs/reviews/2026-10-11-key39-v7-input-keycape-source-review.json\n100644 f4d4d9013656d1039585e32e3a9777eab096fa71 0\tdocs/reviews/2026-10-11-key39-v7-input-keycape-source-review.sources.json\n100644 2b5ccdb5341c43030ff87a62d418e2322d4e75af 0\tdocs/reviews/2026-10-11-key39-v7-input-keycape-work-record.json\n100644 7e4d3ec75f7671b2327f7eccfc51d1d902db275f 0\tdocs/reviews/2026-10-11-key39-v8-input-keycape-source-ack.json\n100644 1d9a58cd94ceba2d4727060eaf13cc0442240352 0\tdocs/reviews/2026-10-11-key39-v8-input-keycape-source-ack.sources.json\n100644 69a2ab4de8cbe2987a2be322648aa90ace9e26d6 0\tdocs/reviews/2026-10-11-key39-v8-input-keycape-work-record.json\n100644 553e90a108f111a9c42b88e16e1f38ff38af3231 0\tdocs/reviews/2026-10-11-key39-v9-v4-keycape-source-ack.json\n100644 524f47af20bae995988f59285c5f29283261e007 0\tdocs/reviews/2026-10-11-key39-v9-v4-keycape-source-ack.sources.json\n100644 acf3e488fa9af74216f669e3f84bdf5d22e3963b 0\tdocs/reviews/2026-10-11-key39-v9-v4-keycape-work-record.json\n100644 2b7e30ef9b82ebbf22ad589d31d0fccb74b33c95 0\tdocs/secrets-engine-openbao-owner-packet.md\n100644 ce5f53365f2ae3b14cd53f4b43c248f77c5a0223 0\tdocs/sitting-requester-registration.json\n100644 d45478c9ef6363a735fd3605417e9275c18a2a94 0\tdocs/t03-requester-registration.json\n100644 32efcf0a5afc5c660bd754eb494020c32f609bdd 0\tdocs/t03-requester-registration.md\n100644 9ab277b85b550ac52ccf98bfda0a0dd7c744e171 0\tdocs/tenant-claim-contract.md\n100644 4e7d26b6a1cbcacf6498aed0e33da8964160c39e 0\tdocs/upstream-issuer-proof.md\n100644 4f305e21acfa7ad7f44b70542ba8f627b5ef9362 0\thistory/2026-09-05-011726-scope-intent-assessment.md\n100644 394eed11f5afb16de7eda7b09f99cf59dbbbacce 0\thistory/2026-09-09-scope-reassessment.md\n100644 2c9e75f72d9983b1b2980d79b12380564465e640 0\thistory/2026-09-28-railiance-admin-role.md\n100644 77233c20c2230459b2d78d9d28578e9472e568ce 0\thistory/KEY-WP-0011-live-secret-exposure-recovery.md\n100644 8eef81bac925dacfa8408bdfb4e5a2d672ba903e 0\thistory/KEY-WP-0011-rotated-credentials-2026-08-23-final.tar.age\n100644 845c3025f781d2296f40b4ec93c43935a480295d 0\tintakes/intakes.md\n100644 569abe99c95e163d39048f4b02ccb47165751c7e 0\tregistry/README.md\n100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0\tregistry/capabilities/.gitkeep\n100644 59cc40e2ccad5b2ce9111366efbc9e48f8e18e70 0\tregistry/capabilities/capability.iam.key-cape.md\n100644 0718f5218c9f4b4ef84c6d9aacd4b61440098405 0\tregistry/indexes/capabilities.yaml\n100755 bf05bb3db90d62b77a1104e48d8e31045ccf0d8e 0\tscripts/bootstrap-dev.sh\n100644 29627c2056fbcf511bce069db0a8803c797295ff 0\tscripts/collect-upstream-issuer-proof.py\n100644 10832318492c4b34230400e458209777d58b08a1 0\tscripts/factor_recovery.py\n100644 2bc28a205ffbd14c4f077c61957f5e3c0af284fb 0\tscripts/keycape_onboarding_guard.py\n100644 e3a875e114d176431b517667c0c096ac4a974ad2 0\tscripts/openbao_negative_private_handoff.py\n100644 4eae60ab0d0766a390743ed33d665ea852bb285e 0\tscripts/openbao_negative_private_launcher.py\n100644 cb864ae0b2b46650a5e20ae90e9465701c362011 0\tscripts/openbao_negative_probe.py\n100644 477857655b8316659d7a3a2afcbe330e497d3851 0\tscripts/provider-browser-fixture.py\n100644 5ac86bfec073a054b712fa7a82fd44066f203a29 0\tscripts/provider-onboarding-contract.py\n100644 2abdf8d67ab9a61f93ab3ef90d36942ee7659702 0\tscripts/provider_browser.mjs\n100644 bc9f59737a06c59e7746aecd00df8112a9c830d1 0\tscripts/provider_browser_acceptance.py\n100644 ca0bb2a3554c569d3923c48bebe0850a1f4254ff 0\tscripts/recovery_service.py\n100644 0f2dca8e890f18bf5430596a1c722bdc4bcb4d0c 0\tscripts/render-upstream-issuer-probe.py\n100755 48a5832ed965624685eeeb9cad396b8e6a854426 0\tscripts/test-scenario-b.sh\n100755 e333ec26464ade014e45d7a13b153d0a6f29b0a1 0\tscripts/test-scenario-c.sh\n100644 25d961f46258329f2290811101388c293e16b025 0\tscripts/test_factor_recovery.py\n100644 c17061849c2a8f9b6c614e69888502607c612674 0\tscripts/test_openbao_negative_private_handoff.py\n100644 e1d98a393d5276d64d689c3b961790219e939f77 0\tscripts/test_openbao_negative_private_launcher.py\n100644 8344b22b4d62deb60ccafafa7acca336d226b0a8 0\tscripts/test_openbao_negative_private_preparation.py\n100644 242d4502ab1f2bda9886ec0936c647019c426696 0\tscripts/test_openbao_negative_probe.py\n100644 6cdb5acd55440ed089e2842f5c762627363defa1 0\tscripts/test_recovery_service.py\n100644 ab6f45297f354d006c4e24beb698bafe8a93a31b 0\tscripts/test_render_upstream_issuer_probe.py\n100644 fca9b9d450cdee91d383ff8a67c738774c5aca4c 0\tspec/canonical-model.yaml\n100644 41cc35711da626fd1986f0da439b230235cac4df 0\tspec/ldap-schema.yaml\n100644 083df0b3e537a5ebb09b9a454f25c68d650fdac8 0\tsrc/Makefile\n100644 faca3748339a924aff723dcb3ed10ec8d737248f 0\tsrc/cmd/keycape-to-keycloak/main.go\n100644 5edcfd7d4fe8f6af021676e8f0f8a0b258831c37 0\tsrc/cmd/keycape/checkconfig.go\n100644 93e4c44654ca28fb16027748c9c24d91d5ced4bd 0\tsrc/cmd/keycape/checkconfig_test.go\n100644 c2b60259d95831503e35a0c67fbe6e3d0c4169f3 0\tsrc/cmd/keycape/clients_test.go\n100644 16de34c66062718b91821d0b836e258c82783401 0\tsrc/cmd/keycape/main.go\n100644 daac6e986c48fcf2de9243ef78a40e0b88b9de4a 0\tsrc/cmd/lldap-export/main.go\n100644 5aa9a2bb1472931b7b9c798fbebfee45fe316f9b 0\tsrc/cmd/lldap-export/main_test.go\n100644 c98b19b31f2c2be0124c88f001b33048a7221968 0\tsrc/cmd/lldap-to-ldap/main.go\n100644 411272078c48cc52abce1007c0939528c66581fa 0\tsrc/cmd/validator/main.go\n100644 992c33f91d6b7f1244634cf583072f4bfbf73759 0\tsrc/go.mod\n100644 d1f81c7aef0bb7c7892e9dd2131653661659bd0b 0\tsrc/go.sum\n100644 b78811acf78adad92d9a412c13cbb7aa86866468 0\tsrc/internal/adapters/authelia/adapter.go\n100644 81ac6205d8217509e6130b159c77e94291ca55a5 0\tsrc/internal/adapters/authelia/adapter_test.go\n100644 810f5b9387721058fb629a305d3aaf5254c65a12 0\tsrc/internal/adapters/authelia/config.go\n100644 082708a178b818d83632bfe31c77696c03530c60 0\tsrc/internal/adapters/authelia/issuer_probe.go\n100644 7943c3f59d5c59aba5adaff9dab1af4d4055d635 0\tsrc/internal/adapters/authelia/issuer_probe_test.go\n100644 7c85d3b9ea7d7843092b24d812807fb6b48755dd 0\tsrc/internal/adapters/authelia/verify.go\n100644 d9003680dc01f3d709d938ff8843f0a96b8a69f6 0\tsrc/internal/adapters/authelia/verify_test.go\n100644 e15cecd7c7ee83383745a367502bf6ab3d1280fe 0\tsrc/internal/adapters/lldap/adapter.go\n100644 e153314a0fc06e33ce8b957b66cc1838ee93a180 0\tsrc/internal/adapters/lldap/adapter_test.go\n100644 fce760a4e97c94e1e542a4d2f0895a20c709fed9 0\tsrc/internal/adapters/lldap/config.go\n100644 d9e10ff898c19e85160e8b047f79a1d9daa797c7 0\tsrc/internal/adapters/lldap/config_internal_test.go\n100644 d76e938ebfc2f676d5dd0a0639707d1e7f4ebc08 0\tsrc/internal/adapters/lldap/railiance_admin_test.go\n100644 35e65335a00ead963c3cd8e95e1b94a5b34e6a5a 0\tsrc/internal/adapters/privacyidea/adapter.go\n100644 286562d56a3f263df68f34eb35bf68b6581688da 0\tsrc/internal/adapters/privacyidea/adapter_test.go\n100644 b34c9f3d19109a7a0acc309c27639ce913f1e751 0\tsrc/internal/adapters/privacyidea/config.go\n100644 017315272e6baa1151d4e73912b19c3bb8ca377b 0\tsrc/internal/adapters/privacyidea/credential_file_test.go\n100644 6f3b883c0c7eced42bfdbe45713b1be5dadf0bc9 0\tsrc/internal/adapters/privacyidea/enrollment_state_test.go\n100644 13316d5ffada681a8f956b519f5a3d6ef94c3233 0\tsrc/internal/adapters/privacyidea/optional_mfa_test.go\n100644 964fbb76048c44616d56b7da916d44c6b86432f0 0\tsrc/internal/adapters/privacyidea/read_scope_test.go\n100644 eeb54b9f0b7e1b9776fc37ecfc36599bbf07026a 0\tsrc/internal/adapters/tenantengine/adapter.go\n100644 d437fd14dcfffa53999b16849888a86c553d0a42 0\tsrc/internal/adapters/tenantengine/adapter_test.go\n100644 2b0417e3745a31e177d2b154454a0f132d2f7616 0\tsrc/internal/authclient/cli.go\n100644 0d39586235d89aa8097d76b7d210331e4a53083f 0\tsrc/internal/authclient/client.go\n100644 dfce39882584ab7f37ecb9878d6b1801b74d9a4c 0\tsrc/internal/authclient/client_test.go\n100644 66b0ea4961b3573152c491f597fad3e02ea59a1a 0\tsrc/internal/authclient/verify.go\n100644 a2a824f02554f0645a543934ee33419dca429085 0\tsrc/internal/authclient/verify_test.go\n100644 f28ad987f862203708795498947229df9de3757e 0\tsrc/internal/config/account_test.go\n100644 7895b73a96899a5443a0a044108e3dd3e4e76294 0\tsrc/internal/config/config.go\n100644 d4236a6d45fa57b865b0983b395c6e9b3974105b 0\tsrc/internal/config/config_test.go\n100644 585aa6243370bb60a94506f5fa670516959c12b0 0\tsrc/internal/config/optional_mfa_test.go\n100644 c020e4076637f2f062ae49a8578bea8dd493bd70 0\tsrc/internal/config/validate.go\n100644 afbc48c673df6fe76ed3f2fa2aea492e476ec37b 0\tsrc/internal/domain/assurance.go\n100644 b9096ef7c09da6a413ed392c751799b3bb1b72bf 0\tsrc/internal/domain/assurance_test.go\n100644 66c907c367651b193559701c2da564aea6757ae8 0\tsrc/internal/domain/auth.go\n100644 5984835dd46da65b095829e05948f841554f733f 0\tsrc/internal/domain/conformance_test.go\n100644 30b0a92d9444324cdf67df1e856feb984385c29e 0\tsrc/internal/domain/mfa.go\n100644 1cfda5371fe0c6eec07329b4990d3c6a105e92ee 0\tsrc/internal/domain/model.go\n100644 56252d821062128e3323a0cdca1064d292a97898 0\tsrc/internal/domain/repository.go\n100644 80ebf0763538369dff67abd3a17c6269022f5e2e 0\tsrc/internal/errors/taxonomy.go\n100644 2f5722e94e982c71b008b7f4c522d500df76a0e1 0\tsrc/internal/errors/taxonomy_test.go\n100644 14769bbe206e8fbbf6eb0ed4212e3ad656ffd996 0\tsrc/internal/jose/jose.go\n100644 34abfa36f8dd355df7713eb940b269f785805402 0\tsrc/internal/jose/jose_test.go\n100644 4119e8a8950a8461c5fe5cb4944af498df5df84f 0\tsrc/internal/migration/lldapexport/exporter.go\n100644 e9071d133a6f3fbd2421d3d0dd9fc8a051d61693 0\tsrc/internal/migration/lldapexport/exporter_test.go\n100644 4666a2b8579e73f76bd4ccf4518cdd9818035e2a 0\tsrc/internal/migration/tokeycloak/transformer.go\n100644 50dee2c66b1922e2d109ca963592fe3a0bf9711c 0\tsrc/internal/migration/tokeycloak/transformer_test.go\n100644 fd21830fd2299a401000bb418946d107bbefa1ee 0\tsrc/internal/migration/toldap/generator.go\n100644 5ec399dca0db878f3d5fab76504947635770727d 0\tsrc/internal/migration/toldap/generator_test.go\n100644 81fd2c8d24016adb626f7a88b442361cf6197124 0\tsrc/internal/server/errors/enforcement.go\n100644 0f8384bc5a147905ea99f33d1a4e573c7a85387e 0\tsrc/internal/server/errors/enforcement_test.go\n100644 7036039edd2837cb22e804b60633b2425a5fb817 0\tsrc/internal/server/oidc/account.go\n100644 1a2d68f1008418e138a6c2d293f2afda017c5ba5 0\tsrc/internal/server/oidc/account_test.go\n100644 72ad579561c84e9ed99da98dd86ca472880ff123 0\tsrc/internal/server/oidc/assurance_time_test.go\n100644 fb5b1f41c0619b7ea6d72c231aec2027769f7f77 0\tsrc/internal/server/oidc/audience_test.go\n100644 60447bc9cd056c2119b1611e38585bc81884a568 0\tsrc/internal/server/oidc/authorize.go\n100644 434b51089fe92497644cdc4504f765f75c21f542 0\tsrc/internal/server/oidc/authorize_test.go\n100644 4b0074b6b5376f11c3979b13b05761c521762bac 0\tsrc/internal/server/oidc/discovery.go\n100644 f2d069d54f67ac1cad061e53687a44ab0c1648ba 0\tsrc/internal/server/oidc/discovery_test.go\n100644 31954ca4dd03ccc36d9da03d5734ac029d46c9ce 0\tsrc/internal/server/oidc/handoff.go\n100644 d8a33fb4e5f90f92e7acab2f198c540d39cfdebc 0\tsrc/internal/server/oidc/handoff_test.go\n100644 d73a3ffcd40558731fb4fac2c66ff0588c0969f5 0\tsrc/internal/server/oidc/hardening_test.go\n100644 187bd993cb85369ada263c734bd4e7daec5bbb73 0\tsrc/internal/server/oidc/human_tenant_test.go\n100644 e74bd2e4a219c25aefc781841e73eb219e0945a5 0\tsrc/internal/server/oidc/jwks.go\n100644 1fd53f19e6c8a131a639d780bea7e1c0c609930a 0\tsrc/internal/server/oidc/jwks_test.go\n100644 7953f06b4948162868166a8ffafef36b2445509b 0\tsrc/internal/server/oidc/login_session.go\n100644 496d237a3f642ac94c869f0dc3438d9a2e262b9d 0\tsrc/internal/server/oidc/logout.go\n100644 47d4df2dee47489a0923df57fe654be87edf2b12 0\tsrc/internal/server/oidc/native_optional_test.go\n100644 2f8eecda6f29cf3d31177ec3b7ec3650a871b555 0\tsrc/internal/server/oidc/optional_mfa_test.go\n100644 221ed02325c872a7947e798607da236f15d7d6e3 0\tsrc/internal/server/oidc/policy_isolation_test.go\n100644 c70d6a4a7d78abd6c0911776f8857ddedb7858bd 0\tsrc/internal/server/oidc/policy_runtime_test.go\n100644 d1c9a9660f3e5fe6c878657957ce6bd7db175c6d 0\tsrc/internal/server/oidc/principal_type_guard_test.go\n100644 15d759c148e72d4a84df18a3d62db1c6838d1be5 0\tsrc/internal/server/oidc/session.go\n100644 ba82ee4b244e241d8d156e05315d8b71626b1cef 0\tsrc/internal/server/oidc/signin_diagnosis.go\n100644 72a1b3518b8824da04d1bf87bd4d73dfa1a7a934 0\tsrc/internal/server/oidc/signin_diagnosis_test.go\n100644 a955146312249d526fa7fedefd085c1140399ac0 0\tsrc/internal/server/oidc/tenant_precondition_test.go\n100644 1517610493b1e91c16f9bfee5fd63361be2d7992 0\tsrc/internal/server/oidc/tenant_test.go\n100644 b1d7f57106208d5cc8a80d964e1698d1dc59dd91 0\tsrc/internal/server/oidc/token.go\n100644 42314ec0fe6c254c3b29c4f859e090f18b316f21 0\tsrc/internal/server/oidc/token_test.go\n100644 c3d2014bd63dc2186d8a2123cef064b2cddfef36 0\tsrc/internal/server/oidc/userinfo.go\n100644 5a12f5e898c8f4526d505404eed14e6f466ca7d7 0\tsrc/internal/server/oidc/userinfo_test.go\n100644 29167da1dbb2b3a9d2e74f5f0319e0902d4173a3 0\tsrc/internal/server/policy/policy.go\n100644 4becee709136b31735eeeab3f1b6b1875de4407e 0\tsrc/internal/server/policy/policy_test.go\n100644 e0cfefbf7c014370890470fc42850c3b3a879daa 0\tsrc/internal/server/readiness/readiness.go\n100644 02cf96ed77554ce25eb4f62e54c00b02cc0089a5 0\tsrc/internal/server/readiness/readiness_test.go\n100644 ff11c05a76db4692e1de6d9ba19f5dc5d45ed2e8 0\tsrc/internal/server/telemetry/emitter.go\n100644 3d64e3d148536f96bddecd228c7eba6711194780 0\tsrc/internal/server/telemetry/emitter_test.go\n100644 793b79bcef80638f2c182645cba9773c17c1550c 0\tsrc/internal/server/telemetry/events.go\n100644 87baa854418f0a6617579e6566aa84217134c6e2 0\tsrc/internal/validator/report.go\n100644 78eb866cef192174b0a29f151eac64f4adc85472 0\tsrc/internal/validator/validator.go\n100644 a86eff31eb55d9cda811adc3508b44e82f6b2eba 0\tsrc/internal/validator/validator_test.go\n100644 75cddf3602a0dbb1d29162db1df187e43e3d4568 0\tsrc/tests/conformance/external_test.go\n100644 208e903a6377e3b894595de8c97248ba96e3dbb3 0\tsrc/tests/migration/fixtures_test.go\n100644 8a49ddec84f156a2ef0badf8f78d292243b7a5dd 0\tsrc/tests/migration/scenario_b_test.go\n100644 bf0a61a1c7e00fd6f3e919529603c5c7461f4a69 0\tsrc/tests/migration/scenario_c_test.go\n100644 377b0f33da592cfbc7eeb9bc74025280ce553a6d 0\tsrc/tests/negative/negative_test.go\n100644 8b802f71346bf27a58ae73877fa594be0557b396 0\tsrc/tests/profile/profile_test.go\n100644 9192fd9cde67896f741cf67dccd48d2250d6ad27 0\ttools/register-informed-decision.py\n100755 0dcb542eeb16827ccb0d0f05b6e302f6af1693e8 0\ttools/rotate-live-exposed-secret.sh\n100644 4b728fd406c815143f302a76512858d3577e6aca 0\twiki/KeyCapeSpecificationPack_v0.1.md\n100644 c803cabd2342689c3123e71b8773edb8a972125d 0\twiki/KeyCapeSpecification_v0.1.md\n100644 f7e78675df62c9b48e29f882fa99298d6d8ce7f1 0\tworkplans/ADHOC-2026-09-05.md\n100644 17401073c053c9710e6e680243bbd313b08d1fc1 0\tworkplans/ADHOC-2026-09-07.md\n100644 0b755547aa7d5589f0898df2f102a45b6aa24838 0\tworkplans/ADHOC-2026-09-27.md\n100644 74334411cd056e7c7253e84f1c4b40c42eb13de3 0\tworkplans/KEY-WP-0001-keycape-implementation.md\n100644 418f477c0ede13d7c003d744abf234347f5d5e31 0\tworkplans/KEY-WP-0002-container-image-gitea.md\n100644 9896c67d78f0efdd03325535eb41295250be3a19 0\tworkplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md\n100644 a50e910558ee43073c92cb27865a1aceac990dd5 0\tworkplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md\n100644 648d2405699dcf25b3121df6c46d871597100d7b 0\tworkplans/KEY-WP-0005-iam-profile-core-claims.md\n100644 632c4c2cad39cbde55c2ae1f2ba5ecf2f445b1a0 0\tworkplans/KEY-WP-0006-client-credentials-service-tokens.md\n100644 f1b04e1a61b398426c23d8d9ca655b3937d7bd10 0\tworkplans/KEY-WP-0007-user-engine-portal-oidc-client.md\n100644 438ab5480de3bd66d75b6ec7e09639d1b93e3e5f 0\tworkplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md\n100644 c9e585db65424c7c4e3b5adf37a68de2bdfbdc1c 0\tworkplans/KEY-WP-0009-provider-capabilities-and-service-identities.md\n100644 81614eeb8c98205d6e5eb3201da10e36b76a21e9 0\tworkplans/KEY-WP-0010-openbao-operator-loopback-callback.md\n100644 f7b906f552f6e3279895a90afbe216b6e78ee5ff 0\tworkplans/KEY-WP-0011-live-secret-exposure-recovery.md\n100644 0f3deb626e5de9a9f898754ef81d3f2ee5282792 0\tworkplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md\n100644 7d77aee3caf3d79fe6d6c1369249c6d94e0ae123 0\tworkplans/KEY-WP-0013-approval-engine-resource-audience.md\n100644 bacf04c07f59125a90c519d3c306a7414cb9a1a3 0\tworkplans/KEY-WP-0014-native-credential-lane-handoff.md\n100644 82b952a216346ffb7f5cab413623fe4a88f7700e 0\tworkplans/KEY-WP-0015-scope-intent-assessment.md\n100644 52839bd0f83d97656c8ad97a8ffc338d9019239a 0\tworkplans/KEY-WP-0016-authorization-code-protocol-hardening.md\n100644 00eb03971aae75724afbf7b8a4946ce6b7eda3af 0\tworkplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md\n100644 325fc483d62dbad53b92b986de56112168a92b4a 0\tworkplans/KEY-WP-0018-export-completeness-evidence.md\n100644 dfeb92296f46d4ce2ac7df1fedcc61e9ccc5d6b3 0\tworkplans/KEY-WP-0019-upstream-provider-token-verification.md\n100644 7b6f3be976293469dee2180c10e16f313876d640 0\tworkplans/KEY-WP-0020-migration-contract-preservation.md\n100644 1f6efeb32a6eb8fcbc52f4d3b591e2fc6b9e006d 0\tworkplans/KEY-WP-0021-snapshot-attribute-validation.md\n100644 7c69629ecd20d0158aa1332d9d1442b796f6a7af 0\tworkplans/KEY-WP-0022-replacement-harness-and-external-conformance.md\n100644 3e7c83cb12e7fc31e682cf3c27f4c4767ded2905 0\tworkplans/KEY-WP-0023-live-migration-proof.md\n100644 baa0bbd195a92708f2befd027b938fcd1812ced7 0\tworkplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md\n100644 a53e646761178748e8c4b00dcde888698447a948 0\tworkplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md\n100644 1f96a2ac3ac939c4e6e0f66172477cf73db04e51 0\tworkplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md\n100644 57e01b2db033847cc786504b8795e37112d988a7 0\tworkplans/KEY-WP-0027-rollout-readiness-and-live-state.md\n100644 46b0672fea90cd7cb672f7d119706c240474f8ca 0\tworkplans/KEY-WP-0028-browser-client-field-validation.md\n100644 392f97750f6cb8b12868a6fd016c7d396b58477e 0\tworkplans/KEY-WP-0029-scope-reassessment.md\n100644 0f74e0d5a037eec64b11b57c67b02057a3b0bfef 0\tworkplans/KEY-WP-0030-tenant-precondition-guard.md\n100644 d2ede9d494927976ccb9ba6a5aeaf1ed21c30837 0\tworkplans/KEY-WP-0031-gate-house-tenant-ruling.md\n100644 a04f52f817a51efc1d0d0ecb99efacea8dc53a17 0\tworkplans/KEY-WP-0032-principal-type-provenance-guard.md\n100644 5b478f953890339aec796c11a536af0ec3238789 0\tworkplans/KEY-WP-0033-vergabe-fresh-login.md\n100644 eb58f99bc26e3c3ac33230dfe432c8b1fabe47ce 0\tworkplans/KEY-WP-0034-account-recovery.md\n100644 29eb711f656c91808e91f3981a9168c3a0ebc253 0\tworkplans/KEY-WP-0035-optional-mfa.md\n100644 fd296bc05a6e565b0ef6e88a9ae3daa7c9f81786 0\tworkplans/KEY-WP-0036-platform-factor-recovery.md\n100644 ad7fed77a27aa8487693ad6fcf342502f418427a 0\tworkplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md\n100644 8cd5bc66f7b0115eec7813d6eec994a1bb618668 0\tworkplans/KEY-WP-0038-forgejo-oidc-client.md\n100644 fd0fc46e49164312208afcc2c53549973b56e1fd 0\tworkplans/KEY-WP-0039-openbao-negative-probe-tokens.md\n100644 8e8730c8e7733b13d6a9f42689a9d8e407769e81 0\tworkplans/KEY-WP-0040-mfa-attended-run-findings.md\n100644 56fd05819e3acd37b8e6cb468ec3d62307f1ae7a 0\tworkplans/KEY-WP-0041-sign-in-diagnosis-page.md\n", + "path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md", + "before_sha256": "c576db4b97ea5de0a50e17f251f8d5c57a744ded557bede963e48608bf3a6ca2", + "after_sha256": "618600946f6dd4128206db7ab23da9cc96e22cb84e0fd301b1b2eacd5c9bdba4", + "frontmatter": { + "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" + }, + "task_blocks": [ + { + "id": "KEY-WP-0039-T01", + "status": "done", + "priority": "medium", + "state_hub_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8" + }, + { + "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" + }, + { + "id": "KEY-WP-0039-T03", + "status": "done", + "priority": "medium", + "state_hub_task_id": "54d37ad8-2352-51c2-bb2e-af6070dbbd73" + } + ], + "approval_scope_source": { + "path": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1/registration-approval-readback.json", + "sha256": "c301f87274a9cc6df873d8a42c1cc999bc3226931602cab0a7c4c02e928d1773" + }, + "assessment": "Clarification needed because previous coordinator wording grouped original-client go with standing rule without naming its object; no historical approval revoked or expanded." +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-receiver.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-receiver.json new file mode 100644 index 00000000..7930ec7f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-receiver.json @@ -0,0 +1,173 @@ +{ + "schema": "state-hub.repository-projection-reconcile.v1", + "instance_role": "primary", + "instance_label": "railiance01", + "expected_commit": "26ec9bb9b195ad377448570d6b161fd682311a0b", + "derived_commit": "26ec9bb9b195ad377448570d6b161fd682311a0b", + "outcome": { + "schema": "state-hub.projection-reset.v1", + "repo_slug": "key-cape", + "commit": "26ec9bb9b195ad377448570d6b161fd682311a0b", + "status": "applied", + "counts": { + "created": 0, + "updated": 44, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 0, + "updated_tasks": 0, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 1, + "external_edges": 8 + }, + "created": [], + "updated": [ + "ADHOC-2026-09-05", + "ADHOC-2026-09-07", + "ADHOC-2026-09-27", + "KEY-WP-0001", + "KEY-WP-0002", + "KEY-WP-0003", + "KEY-WP-0004", + "KEY-WP-0005", + "KEY-WP-0006", + "KEY-WP-0007", + "KEY-WP-0008", + "KEY-WP-0009", + "KEY-WP-0010", + "KEY-WP-0011", + "KEY-WP-0012", + "KEY-WP-0013", + "KEY-WP-0014", + "KEY-WP-0015", + "KEY-WP-0016", + "KEY-WP-0017", + "KEY-WP-0018", + "KEY-WP-0019", + "KEY-WP-0020", + "KEY-WP-0021", + "KEY-WP-0022", + "KEY-WP-0023", + "KEY-WP-0024", + "KEY-WP-0025", + "KEY-WP-0026", + "KEY-WP-0027", + "KEY-WP-0028", + "KEY-WP-0029", + "KEY-WP-0030", + "KEY-WP-0031", + "KEY-WP-0032", + "KEY-WP-0033", + "KEY-WP-0034", + "KEY-WP-0035", + "KEY-WP-0036", + "KEY-WP-0037", + "KEY-WP-0038", + "KEY-WP-0039", + "KEY-WP-0040", + "KEY-WP-0041" + ], + "retired": [], + "released": [], + "created_tasks": [], + "updated_tasks": [], + "cancelled_tasks": [], + "created_edges": [], + "removed_edges": [], + "unresolved_edges": [ + { + "workplan": "KEY-WP-0010", + "from": "KEY-WP-0010", + "target": "RAILIANCE-WP-0027", + "reason": "missing" + } + ], + "external_edges": [ + { + "workplan": "KEY-WP-0038", + "from": "KEY-WP-0038-T03", + "target": "KEY-WP-0038-T02", + "relationship_type": "starts_after", + "from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9", + "from_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115", + "to_workplan_id": null, + "to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30" + }, + { + "workplan": "KEY-WP-0038", + "from": "KEY-WP-0038-T02", + "target": "KEY-WP-0038-T01", + "relationship_type": "starts_after", + "from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9", + "from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30", + "to_workplan_id": null, + "to_task_id": "5f019d13-1fa3-5b88-a27a-75a3339ca96e" + }, + { + "workplan": "KEY-WP-0038", + "from": "KEY-WP-0038-T02", + "target": "NK-WP-0048-T06", + "relationship_type": "starts_after", + "from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9", + "from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30", + "to_workplan_id": null, + "to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" + }, + { + "workplan": "KEY-WP-0039", + "from": "KEY-WP-0039-T02", + "target": "KEY-WP-0038-T03", + "relationship_type": "starts_after", + "from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "to_workplan_id": null, + "to_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115" + }, + { + "workplan": "KEY-WP-0039", + "from": "KEY-WP-0039-T02", + "target": "KEY-WP-0039-T01", + "relationship_type": "starts_after", + "from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "to_workplan_id": null, + "to_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8" + }, + { + "workplan": "KEY-WP-0039", + "from": "KEY-WP-0039-T02", + "target": "KEY-WP-0038-T02", + "relationship_type": "starts_after", + "from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86", + "from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3", + "to_workplan_id": null, + "to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30" + }, + { + "workplan": "KEY-WP-0007", + "from": "KEY-WP-0007", + "target": "KEY-WP-0005", + "relationship_type": "blocks", + "from_workplan_id": "24db4178-705c-5b09-b7db-ee7a345d71a0", + "from_task_id": null, + "to_workplan_id": "711a8303-69aa-5db6-9043-92530c6dc81b", + "to_task_id": null + }, + { + "workplan": "KEY-WP-0009", + "from": "KEY-WP-0009", + "target": "KEY-WP-0006", + "relationship_type": "blocks", + "from_workplan_id": "c1a9b1cc-2ff0-566a-b544-1a2ef967fc0d", + "from_task_id": null, + "to_workplan_id": "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d", + "to_task_id": null + } + ], + "refused": [], + "notes": [] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-reconcile.py b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-reconcile.py new file mode 100644 index 00000000..cc8e2515 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/canonical-reconcile.py @@ -0,0 +1,32 @@ +from pathlib import Path +import hashlib,json,subprocess,urllib.request,urllib.error,yaml,re +root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1');base='http://127.0.0.1:8000';head='26ec9bb9b195ad377448570d6b161fd682311a0b' +def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode() +assert git('rev-parse','HEAD').strip()==head +assert git('ls-remote','origin','refs/heads/main').split()[0]==head +assert not git('status','--porcelain=v1') +def get(path): + with urllib.request.urlopen(base+path,timeout=30) as response:return json.load(response) +health=get('/state/health');assert (health['instance_role'],health['instance_label'])==('primary','railiance01') +request_path='/repos/key-cape/work-record-projection/reconcile';body={'expected_commit':head,'acknowledge_retirements':False} +digest=hashlib.sha256(json.dumps({'method':'POST','path':request_path,'body':body},sort_keys=True,separators=(',',':')).encode()).hexdigest() +request=urllib.request.Request(base+request_path,data=json.dumps(body).encode(),method='POST',headers={'Content-Type':'application/json','Idempotency-Key':'rmgr-projection:v2:'+digest,'X-StateHub-Source-Agent':'custodian-codex'}) +try: + with urllib.request.urlopen(request,timeout=120) as response:receipt=json.load(response) +except urllib.error.HTTPError as error: + raw=error.read();(out/'canonical-receiver-refusal.json').write_bytes(raw);raise +(out/'canonical-receiver.json').write_text(json.dumps(receipt,indent=2)+'\n') +assert receipt['expected_commit']==receipt['derived_commit']==head +assert (receipt['instance_role'],receipt['instance_label'])==('primary','railiance01') +assert receipt['outcome']['status'] in ('applied','noop'),receipt['outcome']['status'] +snapshot=get('/repos/key-cape/work-record-projection/snapshot');baseline=json.loads((out/'baseline.json').read_text()) +planid=baseline['frontmatter']['state_hub_workstream_id'];plan=next(w for w in snapshot['workplans'] if w['id']==planid) +rows=[] +for block in baseline['task_blocks']: + task=next(t for t in snapshot['tasks'] if t['id']==block['state_hub_task_id']) + assert task['status']==block['status'];rows.append({'record_id':block['id'],'state_hub_task_id':task['id'],'status':task['status'],'workplan_id':task.get('workplan_id'),'depends_on_source':block.get('depends_on',[])}) +current=(root/baseline['path']).read_text();assert current==(out/'KEY39.proposed.md').read_text() +assert re.findall(r'^```task\n.*?^```',current,re.M|re.S)==re.findall(r'^```task\n.*?^```',(out/'KEY39.preimage.md').read_text(),re.M|re.S) +verification={'commit':head,'receiver_status':receipt['outcome']['status'],'acknowledge_retirements':False,'instance_role':receipt['instance_role'],'instance_label':receipt['instance_label'],'workplan':{'id':planid,'status':plan['status'],'record_id':'KEY-WP-0039'},'task_bindings':rows,'all_three_source_blocks_unchanged':True,'frontmatter_unchanged':True,'only_committed_path':baseline['path'],'remote_forge_matches':True,'owner_worktree_and_index_clean':not git('status','--porcelain=v1'),'normal_sync_refusal':'unrelated grandfathered ADHOC-2026-09-27-T02 UUID; not modified','native_acceptance_inferred':False} +assert plan['status']==baseline['frontmatter']['status'] +(out/'final-verification.json').write_text(json.dumps(verification,indent=2)+'\n');print(json.dumps(verification,indent=2)) diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/commit.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/commit.json new file mode 100644 index 00000000..1a69d3af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/commit.json @@ -0,0 +1,9 @@ +{ + "commit": "26ec9bb9b195ad377448570d6b161fd682311a0b", + "parent": "40de616e078906e04c692edf7cb7a8bfbc07b5ea", + "paths": [ + "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md" + ], + "normal_hook": "scanner8.30.1 clean", + "native_or_credential_action": false +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/.custodian-brief.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/.custodian-brief.md new file mode 100644 index 00000000..d3b36c3b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/.custodian-brief.md @@ -0,0 +1,37 @@ + +# Custodian Brief — key-cape + +**Domain:** infotech +**Last synced:** 2026-10-17 02:16 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Negative-check tokens for the secrets-engine OpenBao JWT role +Progress: 2/3 done | workplan_id: `09c3bddf-6058-5603-8488-cccdb7be4f86` + +**Open tasks:** +- ! T01 — Define the probe registrations `03da4c38` + *(wait: Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.)* + +### Forgejo as a NetKingdom OIDC client, so the forge can close +Progress: 2/4 done | workplan_id: `098e3e2c-a138-560e-b2d3-3a15eb49d8a9` + +**Open tasks:** +- ! T01 — Define what Forgejo receives `ddcfb482` + *(wait: Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout.)* +- ! T02 — Register the client and hand over its secret `40ddaa29` + *(wait: Claims are verified against the registered client.)* + +## Inbox Hygiene + +**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start. +**Missing thread_id:** 2 unread message(s) lack supersession chains. + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/WORK-RECORDS.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/WORK-RECORDS.md new file mode 100644 index 00000000..d86b29bb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/WORK-RECORDS.md @@ -0,0 +1,224 @@ +# Work Records — key-cape + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | ADHOC-2026-09-05 | finished | — | workplans/ADHOC-2026-09-05.md | +| workplan | ADHOC-2026-09-07 | finished | — | workplans/ADHOC-2026-09-07.md | +| workplan | ADHOC-2026-09-27 | finished | — | workplans/ADHOC-2026-09-27.md | +| workplan | KEY-WP-0001 | finished | — | workplans/KEY-WP-0001-keycape-implementation.md | +| workplan | KEY-WP-0002 | finished | — | workplans/KEY-WP-0002-container-image-gitea.md | +| workplan | KEY-WP-0003 | finished | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| workplan | KEY-WP-0004 | finished | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| workplan | KEY-WP-0005 | finished | — | workplans/KEY-WP-0005-iam-profile-core-claims.md | +| workplan | KEY-WP-0006 | finished | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| workplan | KEY-WP-0007 | finished | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md | +| workplan | KEY-WP-0008 | finished | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md | +| workplan | KEY-WP-0009 | finished | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md | +| workplan | KEY-WP-0010 | finished | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md | +| workplan | KEY-WP-0011 | finished | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | +| workplan | KEY-WP-0012 | finished | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md | +| workplan | KEY-WP-0013 | finished | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| workplan | KEY-WP-0014 | finished | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md | +| workplan | KEY-WP-0015 | finished | — | workplans/KEY-WP-0015-scope-intent-assessment.md | +| workplan | KEY-WP-0016 | finished | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md | +| workplan | KEY-WP-0017 | finished | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md | +| workplan | KEY-WP-0018 | finished | — | workplans/KEY-WP-0018-export-completeness-evidence.md | +| workplan | KEY-WP-0019 | finished | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| workplan | KEY-WP-0020 | finished | — | workplans/KEY-WP-0020-migration-contract-preservation.md | +| workplan | KEY-WP-0021 | finished | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md | +| workplan | KEY-WP-0022 | finished | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| workplan | KEY-WP-0023 | finished | — | workplans/KEY-WP-0023-live-migration-proof.md | +| workplan | KEY-WP-0024 | finished | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md | +| workplan | KEY-WP-0025 | finished | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md | +| workplan | KEY-WP-0026 | finished | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| workplan | KEY-WP-0027 | finished | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md | +| workplan | KEY-WP-0028 | finished | — | workplans/KEY-WP-0028-browser-client-field-validation.md | +| workplan | KEY-WP-0029 | finished | — | workplans/KEY-WP-0029-scope-reassessment.md | +| workplan | KEY-WP-0030 | finished | — | workplans/KEY-WP-0030-tenant-precondition-guard.md | +| workplan | KEY-WP-0031 | finished | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md | +| workplan | KEY-WP-0032 | finished | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md | +| workplan | KEY-WP-0033 | finished | — | workplans/KEY-WP-0033-vergabe-fresh-login.md | +| workplan | KEY-WP-0034 | finished | — | workplans/KEY-WP-0034-account-recovery.md | +| workplan | KEY-WP-0035 | finished | — | workplans/KEY-WP-0035-optional-mfa.md | +| workplan | KEY-WP-0036 | finished | — | workplans/KEY-WP-0036-platform-factor-recovery.md | +| workplan | KEY-WP-0037 | finished | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md | +| workplan | KEY-WP-0038 | active | — | workplans/KEY-WP-0038-forgejo-oidc-client.md | +| workplan | KEY-WP-0039 | active | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md | +| workplan | KEY-WP-0040 | finished | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md | +| workplan | KEY-WP-0041 | finished | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md | +| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md | +| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md | +| task | ADHOC-2026-09-27-T01 | done | — | workplans/ADHOC-2026-09-27.md | +| task | ADHOC-2026-09-27-T02 | done | — | workplans/ADHOC-2026-09-27.md | +| task | KEY-WP-0001-T01 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T02 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T03 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T04 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T05 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T06 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T07 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T08 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T09 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T10 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T11 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T12 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T13 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T14 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T15 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T16 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T17 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T18 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T19 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T20 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T21 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T22 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0001-T23 | done | — | workplans/KEY-WP-0001-keycape-implementation.md | +| task | KEY-WP-0002-T01 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0002-T02 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0002-T03 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0002-T04 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0002-T05 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0002-T06 | done | — | workplans/KEY-WP-0002-container-image-gitea.md | +| task | KEY-WP-0003-T01 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| task | KEY-WP-0003-T02 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| task | KEY-WP-0003-T03 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| task | KEY-WP-0003-T04 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| task | KEY-WP-0003-T05 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md | +| task | KEY-WP-0004-T01 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T02 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T03 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T04 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T05 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T06 | cancel | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0004-T07 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md | +| task | KEY-WP-0005-T01 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md | +| task | KEY-WP-0005-T02 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md | +| task | KEY-WP-0005-T03 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md | +| task | KEY-WP-0006-T01 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| task | KEY-WP-0006-T02 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| task | KEY-WP-0006-T03 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| task | KEY-WP-0006-T04 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| task | KEY-WP-0006-T05 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md | +| task | KEY-WP-0007-T01 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md | +| task | KEY-WP-0007-T02 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md | +| task | KEY-WP-0007-T03 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md | +| task | KEY-WP-0008-T01 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md | +| task | KEY-WP-0008-T02 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md | +| task | KEY-WP-0008-T03 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md | +| task | KEY-WP-0008-T04 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md | +| task | KEY-WP-0009-T01 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md | +| task | KEY-WP-0009-T02 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md | +| task | KEY-WP-0009-T03 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md | +| task | KEY-WP-0009-T04 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md | +| task | KEY-WP-0010-T01 | done | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md | +| task | KEY-WP-0011-T01 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | +| task | KEY-WP-0011-T02 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | +| task | KEY-WP-0011-T03 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | +| task | KEY-WP-0012-T01 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md | +| task | KEY-WP-0012-T02 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md | +| task | KEY-WP-0012-T03 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md | +| task | KEY-WP-0013-T01 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T02 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T03 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T04 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T05 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T06 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0013-T07 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md | +| task | KEY-WP-0014-T01 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md | +| task | KEY-WP-0014-T02 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md | +| task | KEY-WP-0014-T03 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md | +| task | KEY-WP-0014-T04 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md | +| task | KEY-WP-0015-T01 | done | — | workplans/KEY-WP-0015-scope-intent-assessment.md | +| task | KEY-WP-0016-T01 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md | +| task | KEY-WP-0016-T02 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md | +| task | KEY-WP-0016-T03 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md | +| task | KEY-WP-0016-T04 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md | +| task | KEY-WP-0017-T01 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md | +| task | KEY-WP-0017-T02 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md | +| task | KEY-WP-0017-T03 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md | +| task | KEY-WP-0017-T04 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md | +| task | KEY-WP-0018-T01 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md | +| task | KEY-WP-0018-T02 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md | +| task | KEY-WP-0018-T03 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md | +| task | KEY-WP-0019-T01 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| task | KEY-WP-0019-T02 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| task | KEY-WP-0019-T03 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| task | KEY-WP-0019-T04 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| task | KEY-WP-0019-T05 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md | +| task | KEY-WP-0020-T01 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md | +| task | KEY-WP-0020-T02 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md | +| task | KEY-WP-0020-T03 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md | +| task | KEY-WP-0021-T01 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md | +| task | KEY-WP-0021-T02 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md | +| task | KEY-WP-0021-T03 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md | +| task | KEY-WP-0022-T01 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| task | KEY-WP-0022-T02 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| task | KEY-WP-0022-T03 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| task | KEY-WP-0022-T04 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| task | KEY-WP-0022-T05 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md | +| task | KEY-WP-0023-T01 | done | — | workplans/KEY-WP-0023-live-migration-proof.md | +| task | KEY-WP-0023-T02 | done | — | workplans/KEY-WP-0023-live-migration-proof.md | +| task | KEY-WP-0023-T03 | done | — | workplans/KEY-WP-0023-live-migration-proof.md | +| task | KEY-WP-0023-T04 | done | — | workplans/KEY-WP-0023-live-migration-proof.md | +| task | KEY-WP-0023-T05 | done | — | workplans/KEY-WP-0023-live-migration-proof.md | +| task | KEY-WP-0024-T01 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md | +| task | KEY-WP-0024-T02 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md | +| task | KEY-WP-0025-T01 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md | +| task | KEY-WP-0025-T02 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md | +| task | KEY-WP-0025-T03 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md | +| task | KEY-WP-0025-T04 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md | +| task | KEY-WP-0026-T01 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0026-T02 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0026-T03 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0026-T04 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0026-T05 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0026-T06 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md | +| task | KEY-WP-0027-T01 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md | +| task | KEY-WP-0027-T02 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md | +| task | KEY-WP-0027-T03 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md | +| task | KEY-WP-0027-T04 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md | +| task | KEY-WP-0028-T01 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md | +| task | KEY-WP-0028-T02 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md | +| task | KEY-WP-0029-T01 | done | — | workplans/KEY-WP-0029-scope-reassessment.md | +| task | KEY-WP-0029-T02 | done | — | workplans/KEY-WP-0029-scope-reassessment.md | +| task | KEY-WP-0029-T03 | done | — | workplans/KEY-WP-0029-scope-reassessment.md | +| task | KEY-WP-0030-T01 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md | +| task | KEY-WP-0030-T02 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md | +| task | KEY-WP-0031-T01 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md | +| task | KEY-WP-0031-T02 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md | +| task | KEY-WP-0032-T01 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md | +| task | KEY-WP-0032-T02 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md | +| task | KEY-WP-0033-T01 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md | +| task | KEY-WP-0033-T02 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md | +| task | KEY-WP-0034-T01 | done | — | workplans/KEY-WP-0034-account-recovery.md | +| task | KEY-WP-0034-T02 | done | — | workplans/KEY-WP-0034-account-recovery.md | +| task | KEY-WP-0035-T01 | done | — | workplans/KEY-WP-0035-optional-mfa.md | +| task | KEY-WP-0035-T02 | done | — | workplans/KEY-WP-0035-optional-mfa.md | +| task | KEY-WP-0035-T03 | done | — | workplans/KEY-WP-0035-optional-mfa.md | +| task | KEY-WP-0035-T04 | done | — | workplans/KEY-WP-0035-optional-mfa.md | +| task | KEY-WP-0035-T05 | done | — | workplans/KEY-WP-0035-optional-mfa.md | +| task | KEY-WP-0036-T01 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md | +| task | KEY-WP-0036-T02 | cancel | — | workplans/KEY-WP-0036-platform-factor-recovery.md | +| task | KEY-WP-0036-T03 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md | +| task | KEY-WP-0037-T01 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md | +| task | KEY-WP-0037-T02 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md | +| task | KEY-WP-0037-T03 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md | +| task | KEY-WP-0037-T04 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md | +| task | KEY-WP-0038-T01 | done | — | workplans/KEY-WP-0038-forgejo-oidc-client.md | +| task | KEY-WP-0038-T02 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md | +| task | KEY-WP-0038-T03 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md | +| task | KEY-WP-0038-T04 | cancel | — | workplans/KEY-WP-0038-forgejo-oidc-client.md | +| task | KEY-WP-0039-T01 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md | +| task | KEY-WP-0039-T02 | wait | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md | +| task | KEY-WP-0039-T03 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md | +| task | KEY-WP-0040-T01 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md | +| task | KEY-WP-0040-T02 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md | +| task | KEY-WP-0041-T01 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md | +| task | KEY-WP-0041-T02 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md | +| intake | KEY-IN-0001 | closed | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/originals.json b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/originals.json new file mode 100644 index 00000000..6e7d1655 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/originals.json @@ -0,0 +1,232 @@ +{ + ".custodian-brief.md": { + "sha256": "96d84539b3a92786bff37fcc70161adec4648b9547287e52e6b817bc080a080a", + "mode": 436, + "index": "100644 d3b36c3bffbeb922769a2a788e35b4113fccaa3f 0\t.custodian-brief.md\n" + }, + "WORK-RECORDS.md": { + "sha256": "431842547bd82750f284f8c1c9f519892f1bb1c1a684b9b1e38adc6fdfdc299d", + "mode": 420, + "index": "100644 d86b29bbc480acfa6d12d5052039578d7d4e380c 0\tWORK-RECORDS.md\n" + }, + "workplans/ADHOC-2026-09-05.md": { + "sha256": "76123d22d0c1efc51212f8914e386f078b3c6d908959af6978ae73874e492b04", + "mode": 420, + "index": "100644 f7e78675df62c9b48e29f882fa99298d6d8ce7f1 0\tworkplans/ADHOC-2026-09-05.md\n" + }, + "workplans/ADHOC-2026-09-07.md": { + "sha256": "c335ebfe91dad291277e8f17034ac8f785b3bc767f281f205c446c08f03d7978", + "mode": 420, + "index": "100644 17401073c053c9710e6e680243bbd313b08d1fc1 0\tworkplans/ADHOC-2026-09-07.md\n" + }, + "workplans/ADHOC-2026-09-27.md": { + "sha256": "2794d1940fc3c5e4a2df57de9d4b742fa6aa8602b67f1d3177c0d96bc5aaf314", + "mode": 420, + "index": "100644 0b755547aa7d5589f0898df2f102a45b6aa24838 0\tworkplans/ADHOC-2026-09-27.md\n" + }, + "workplans/KEY-WP-0001-keycape-implementation.md": { + "sha256": "29a2bf4892b7185e28e9126a3d8311d0fed34a876f416a3e75c605915fde0969", + "mode": 420, + "index": "100644 74334411cd056e7c7253e84f1c4b40c42eb13de3 0\tworkplans/KEY-WP-0001-keycape-implementation.md\n" + }, + "workplans/KEY-WP-0002-container-image-gitea.md": { + "sha256": "76fee187185b30219bc257ccc9b587661df4489574a2bb57703a365a5d803cec", + "mode": 420, + "index": "100644 418f477c0ede13d7c003d744abf234347f5d5e31 0\tworkplans/KEY-WP-0002-container-image-gitea.md\n" + }, + "workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md": { + "sha256": "5e949c696e587540d305b682eb1f8390ec544bbec6a566be8d04e73fb1b254ca", + "mode": 420, + "index": "100644 9896c67d78f0efdd03325535eb41295250be3a19 0\tworkplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md\n" + }, + "workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md": { + "sha256": "260ba7d3116db3bc5a72e5cd8b9984137e7e372c8128d21073614dbd948c10c2", + "mode": 420, + "index": "100644 a50e910558ee43073c92cb27865a1aceac990dd5 0\tworkplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md\n" + }, + "workplans/KEY-WP-0005-iam-profile-core-claims.md": { + "sha256": "c6702b644cdd046c0d3bba8ba8837050826e4dc3d855bd73327ae94218ba5b57", + "mode": 420, + "index": "100644 648d2405699dcf25b3121df6c46d871597100d7b 0\tworkplans/KEY-WP-0005-iam-profile-core-claims.md\n" + }, + "workplans/KEY-WP-0006-client-credentials-service-tokens.md": { + "sha256": "b3227849978c95cad89e02afb74bf2217b6188ae65c5b0af6a21703d65477701", + "mode": 420, + "index": "100644 632c4c2cad39cbde55c2ae1f2ba5ecf2f445b1a0 0\tworkplans/KEY-WP-0006-client-credentials-service-tokens.md\n" + }, + "workplans/KEY-WP-0007-user-engine-portal-oidc-client.md": { + "sha256": "5408ff6dfeb58a8a7756b48fe4f54f7c7b98d7940cb9aaa5c7864f1247e58150", + "mode": 420, + "index": "100644 f1b04e1a61b398426c23d8d9ca655b3937d7bd10 0\tworkplans/KEY-WP-0007-user-engine-portal-oidc-client.md\n" + }, + "workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md": { + "sha256": "bfc9b2118f0712e92a86bbb0f14a2857fafc3a42a3c14c0e2535002aaa57a2f5", + "mode": 420, + "index": "100644 438ab5480de3bd66d75b6ec7e09639d1b93e3e5f 0\tworkplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md\n" + }, + "workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md": { + "sha256": "d1daac9815930104a392ea96c14922e34f947720a31d1b0ac45a770997e0f476", + "mode": 420, + "index": "100644 c9e585db65424c7c4e3b5adf37a68de2bdfbdc1c 0\tworkplans/KEY-WP-0009-provider-capabilities-and-service-identities.md\n" + }, + "workplans/KEY-WP-0010-openbao-operator-loopback-callback.md": { + "sha256": "41d28a16bef9313b776c5c6305316a9e7d4f5528b7fb7427b8384a0ed0ad35c4", + "mode": 420, + "index": "100644 81614eeb8c98205d6e5eb3201da10e36b76a21e9 0\tworkplans/KEY-WP-0010-openbao-operator-loopback-callback.md\n" + }, + "workplans/KEY-WP-0011-live-secret-exposure-recovery.md": { + "sha256": "3c5458180fe81a04e357f8db737e4287f79640b5ab02682bcadc595b73f846cd", + "mode": 420, + "index": "100644 f7b906f552f6e3279895a90afbe216b6e78ee5ff 0\tworkplans/KEY-WP-0011-live-secret-exposure-recovery.md\n" + }, + "workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md": { + "sha256": "68d34d75c914e7d2d8177000e800d6372cccda2b850c03ab85da42b90a66b91c", + "mode": 420, + "index": "100644 0f3deb626e5de9a9f898754ef81d3f2ee5282792 0\tworkplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md\n" + }, + "workplans/KEY-WP-0013-approval-engine-resource-audience.md": { + "sha256": "55c88bf4ed9a456db59b319bbaf960dfe6f4147437e339f950e0583d7582057a", + "mode": 420, + "index": "100644 7d77aee3caf3d79fe6d6c1369249c6d94e0ae123 0\tworkplans/KEY-WP-0013-approval-engine-resource-audience.md\n" + }, + "workplans/KEY-WP-0014-native-credential-lane-handoff.md": { + "sha256": "22aac43ae5039aebd11f9f0aa57a882ad009ad6ed119ec08eb85b32a5af48cd7", + "mode": 420, + "index": "100644 bacf04c07f59125a90c519d3c306a7414cb9a1a3 0\tworkplans/KEY-WP-0014-native-credential-lane-handoff.md\n" + }, + "workplans/KEY-WP-0015-scope-intent-assessment.md": { + "sha256": "60b9f2265eff880d916c3ee8dba36aec9984ec310804de776cebf89cc98fdb95", + "mode": 420, + "index": "100644 82b952a216346ffb7f5cab413623fe4a88f7700e 0\tworkplans/KEY-WP-0015-scope-intent-assessment.md\n" + }, + "workplans/KEY-WP-0016-authorization-code-protocol-hardening.md": { + "sha256": "ed48128a1cb51cbe3d154727bafd5a5ffb3b5b618c20649b2a0e7db154cf3a0c", + "mode": 420, + "index": "100644 52839bd0f83d97656c8ad97a8ffc338d9019239a 0\tworkplans/KEY-WP-0016-authorization-code-protocol-hardening.md\n" + }, + "workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md": { + "sha256": "8700e0e5aa07dcd7f651e792cca12a0330b7ee9d2f6064b4cc3fcf4c7d509340", + "mode": 420, + "index": "100644 00eb03971aae75724afbf7b8a4946ce6b7eda3af 0\tworkplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md\n" + }, + "workplans/KEY-WP-0018-export-completeness-evidence.md": { + "sha256": "15026b708d116f803b0b2fa43f30a4dbced76e4d8137b5fcb5b4275e593d7c7b", + "mode": 420, + "index": "100644 325fc483d62dbad53b92b986de56112168a92b4a 0\tworkplans/KEY-WP-0018-export-completeness-evidence.md\n" + }, + "workplans/KEY-WP-0019-upstream-provider-token-verification.md": { + "sha256": "0d2d8b5bda1e0d333935812d8d3749cb7b0c5523a3987d1b26185f9f6d0f0fa2", + "mode": 420, + "index": "100644 dfeb92296f46d4ce2ac7df1fedcc61e9ccc5d6b3 0\tworkplans/KEY-WP-0019-upstream-provider-token-verification.md\n" + }, + "workplans/KEY-WP-0020-migration-contract-preservation.md": { + "sha256": "cc842d15bf42ddc4d0f4056bc19b3176cab2c5f3c1a2fe6d648e5dbd077be8b3", + "mode": 420, + "index": "100644 7b6f3be976293469dee2180c10e16f313876d640 0\tworkplans/KEY-WP-0020-migration-contract-preservation.md\n" + }, + "workplans/KEY-WP-0021-snapshot-attribute-validation.md": { + "sha256": "b603248693073a9b9b25b4d0a4147d8d88c16a07dc9936e48fa1f3c76fe8bab6", + "mode": 420, + "index": "100644 1f6efeb32a6eb8fcbc52f4d3b591e2fc6b9e006d 0\tworkplans/KEY-WP-0021-snapshot-attribute-validation.md\n" + }, + "workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md": { + "sha256": "8c135e84be81de31b4949f52ac28dccd404f2e4c3eaa576d733bd50160427a53", + "mode": 420, + "index": "100644 7c69629ecd20d0158aa1332d9d1442b796f6a7af 0\tworkplans/KEY-WP-0022-replacement-harness-and-external-conformance.md\n" + }, + "workplans/KEY-WP-0023-live-migration-proof.md": { + "sha256": "e581cd11f3905805b5e8ba8e2ede85092577c7d086e1761f2a2d033fdead4e47", + "mode": 420, + "index": "100644 3e7c83cb12e7fc31e682cf3c27f4c4767ded2905 0\tworkplans/KEY-WP-0023-live-migration-proof.md\n" + }, + "workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md": { + "sha256": "af7842fa97263bfd46a4b5ae84c5c1ecddb06ab9623586e0292dcc93099fea35", + "mode": 420, + "index": "100644 baa0bbd195a92708f2befd027b938fcd1812ced7 0\tworkplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md\n" + }, + "workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md": { + "sha256": "0addb34e6d85e376c6a5db7afba597b6f268afa98b86c8c60add9be174dd1b26", + "mode": 420, + "index": "100644 a53e646761178748e8c4b00dcde888698447a948 0\tworkplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md\n" + }, + "workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md": { + "sha256": "c1bc0de47511ae49102009a172c84d9d5d64e829a18c8d40564fe817935563b3", + "mode": 420, + "index": "100644 1f96a2ac3ac939c4e6e0f66172477cf73db04e51 0\tworkplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md\n" + }, + "workplans/KEY-WP-0027-rollout-readiness-and-live-state.md": { + "sha256": "f559eae61d433d451f80ea575254bb14a47f2485267b412e42d524b434bb1b59", + "mode": 420, + "index": "100644 57e01b2db033847cc786504b8795e37112d988a7 0\tworkplans/KEY-WP-0027-rollout-readiness-and-live-state.md\n" + }, + "workplans/KEY-WP-0028-browser-client-field-validation.md": { + "sha256": "fbd51422fa5e6449000e4520e6639f6135c7393165a5ae02b1e92892edba0480", + "mode": 420, + "index": "100644 46b0672fea90cd7cb672f7d119706c240474f8ca 0\tworkplans/KEY-WP-0028-browser-client-field-validation.md\n" + }, + "workplans/KEY-WP-0029-scope-reassessment.md": { + "sha256": "3241f3b94db9cad5e3770cf1fedc5c211288357e4f69b5ba9da57065094bdf0e", + "mode": 420, + "index": "100644 392f97750f6cb8b12868a6fd016c7d396b58477e 0\tworkplans/KEY-WP-0029-scope-reassessment.md\n" + }, + "workplans/KEY-WP-0030-tenant-precondition-guard.md": { + "sha256": "c94c3afc7fb4270dfc18f3fe8a32946c524e4a6ba2d02a3d8fda69c9b8441af0", + "mode": 420, + "index": "100644 0f74e0d5a037eec64b11b57c67b02057a3b0bfef 0\tworkplans/KEY-WP-0030-tenant-precondition-guard.md\n" + }, + "workplans/KEY-WP-0031-gate-house-tenant-ruling.md": { + "sha256": "0abb135367f0366aa071769c4bd76fdeb321d810ec75ad3b58d718734f78382b", + "mode": 420, + "index": "100644 d2ede9d494927976ccb9ba6a5aeaf1ed21c30837 0\tworkplans/KEY-WP-0031-gate-house-tenant-ruling.md\n" + }, + "workplans/KEY-WP-0032-principal-type-provenance-guard.md": { + "sha256": "6a5cd483a40b9477227b02156c42b3469094dac145b33727b336118898aa782a", + "mode": 420, + "index": "100644 a04f52f817a51efc1d0d0ecb99efacea8dc53a17 0\tworkplans/KEY-WP-0032-principal-type-provenance-guard.md\n" + }, + "workplans/KEY-WP-0033-vergabe-fresh-login.md": { + "sha256": "c01bb51cf2dcad06ea846040dddef09845041fa7271cd7015e5a34a49e60bb8f", + "mode": 420, + "index": "100644 5b478f953890339aec796c11a536af0ec3238789 0\tworkplans/KEY-WP-0033-vergabe-fresh-login.md\n" + }, + "workplans/KEY-WP-0034-account-recovery.md": { + "sha256": "7db9adcd418ccabfbce672aec283249a6e374212492f0349e8c47a568f130eda", + "mode": 420, + "index": "100644 eb58f99bc26e3c3ac33230dfe432c8b1fabe47ce 0\tworkplans/KEY-WP-0034-account-recovery.md\n" + }, + "workplans/KEY-WP-0035-optional-mfa.md": { + "sha256": "c60ea8d0c195686c0d2317f657aad2de27c9b9432d311627c0e1d06124cabb49", + "mode": 420, + "index": "100644 29eb711f656c91808e91f3981a9168c3a0ebc253 0\tworkplans/KEY-WP-0035-optional-mfa.md\n" + }, + "workplans/KEY-WP-0036-platform-factor-recovery.md": { + "sha256": "5ceb08fe5789b4f02550bb04fcaf8eeb25a43ac3f506dfc62d60a513bcd0013f", + "mode": 420, + "index": "100644 fd296bc05a6e565b0ef6e88a9ae3daa7c9f81786 0\tworkplans/KEY-WP-0036-platform-factor-recovery.md\n" + }, + "workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md": { + "sha256": "3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a", + "mode": 420, + "index": "100644 ad7fed77a27aa8487693ad6fcf342502f418427a 0\tworkplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md\n" + }, + "workplans/KEY-WP-0038-forgejo-oidc-client.md": { + "sha256": "dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064", + "mode": 420, + "index": "100644 8cd5bc66f7b0115eec7813d6eec994a1bb618668 0\tworkplans/KEY-WP-0038-forgejo-oidc-client.md\n" + }, + "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md": { + "sha256": "618600946f6dd4128206db7ab23da9cc96e22cb84e0fd301b1b2eacd5c9bdba4", + "mode": 420, + "index": "100644 b22aa6a1116412ab60586163a4fd759270f1b6bf 0\tworkplans/KEY-WP-0039-openbao-negative-probe-tokens.md\n" + }, + "workplans/KEY-WP-0040-mfa-attended-run-findings.md": { + "sha256": "98b23d8810bff047067169232213cb313ecc14ff79751224842723afe808fbd2", + "mode": 420, + "index": "100644 8e8730c8e7733b13d6a9f42689a9d8e407769e81 0\tworkplans/KEY-WP-0040-mfa-attended-run-findings.md\n" + }, + "workplans/KEY-WP-0041-sign-in-diagnosis-page.md": { + "sha256": "7e44f46acd6b3942c1852b49e0d64b205dfd7d9bb378aed5241b1bf22d7ce88a", + "mode": 420, + "index": "100644 56fd05819e3acd37b8e6cb468ec3d62307f1ae7a 0\tworkplans/KEY-WP-0041-sign-in-diagnosis-page.md\n" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-05.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-05.md new file mode 100644 index 00000000..f7e78675 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-05.md @@ -0,0 +1,27 @@ +--- +id: ADHOC-2026-09-05 +type: workplan +title: "Repository review and layer declaration" +domain: infotech +repo: key-cape +status: finished +owner: codex +created: "2026-09-05" +updated: "2026-09-05" +state_hub_workstream_id: "6f5a12f0-5dac-5e02-bbd9-9ab2263264ba" +--- + +## Resolve declaration intake and normalize repository records + +```task +id: ADHOC-2026-09-05-T01 +status: done +priority: low +state_hub_task_id: "54c89abe-2539-550d-bc3a-649af8d4580a" +``` + +Reviewed KEY-IN-0001 against local security-layer-model_v0.4. Declared Tooling +and the authentication/authorization boundary in INTENT.md and closed the intake. +Normalized two legacy completed workplan frontmatter statuses to finished. +Ignored the generated .repo-manager workstation cache. Existing task statuses +already conform to the State Hub task lifecycle canon. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-07.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-07.md new file mode 100644 index 00000000..17401073 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-07.md @@ -0,0 +1,36 @@ +--- +id: ADHOC-2026-09-07 +type: workplan +title: "Document the KEY-WP-0016 authorization-code bindings for consumers" +domain: infotech +repo: key-cape +status: finished +owner: claude +created: "2026-09-07" +updated: "2026-09-07" +state_hub_workstream_id: "c2fdc5cc-89be-561d-b832-f0b3019ea1c7" +--- + +## Publish the consumer-facing rollout note + +```task +id: ADHOC-2026-09-07-T01 +status: done +priority: medium +state_hub_task_id: "2187cef2-07c0-5ac1-89ca-3bb25a160fa4" +``` + +KEY-WP-0016 changed `/token` and `/userinfo` behaviour without a consumer-facing +note; no document in `docs/` mentioned `redirect_uri` at all, so the change would +have reached a deployment silently. + +Published `docs/authorization-code-bindings.md` stating what an exchange must +send, who is affected and how to roll out. Verified the blast radius rather than +assuming it: all five browser registrations in `config/dev-config.yaml` +(`openbao-admin`, `demo-app`, `netkingdom-bootstrap-console`, +`user-engine-portal`, `coulomb-social`) are `clientType: public` with +`grantTypes: ["authorization_code"]`, so the grant-type and confidential-client +bindings cannot affect them and only the `redirect_uri` requirement can. Named +the realistic failure — a client that sends `redirect_uri` to `/authorize` but +omits it at `/token` — and said plainly that it has not been observed against a +live consumer. Referenced the note from SCOPE.md. diff --git a/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-r1/consistency-backup/workplans/ADHOC-2026-09-27.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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-scope-clarification-r1/consistency-backup/workplans/KEY-WP-0034-account-recovery.md b/docs/changes/netkingdom-target-2026-10-17/key39-finalization-scope-clarification-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-scope-clarification-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`.