diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 932bfd9..a80a97d 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -345,7 +345,7 @@ | task | NK-WP-0056-T06 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T07 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T08 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | -| task | NK-WP-0056-T09 | wait | — | workplans/NK-WP-0056-bounded-executor-path.md | +| task | NK-WP-0056-T09 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T10 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | diff --git a/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/10-teardown.json b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/10-teardown.json new file mode 100644 index 0000000..e41d8be --- /dev/null +++ b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/10-teardown.json @@ -0,0 +1,12 @@ +{ + "action": "teardown", + "identity": "syn-journey", + "keycape_client_removal_separate": true, + "kv_removed": true, + "lldap_user_removed": true, + "phase": "done", + "pi_token_removed": true, + "source_sha256": "a213b5f9fc6703dfc5ad619320a2fd083596ece7a2a1895f42829159bea42dde", + "status": "identity_removed", + "values_recorded": false +} diff --git a/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/11-setup.json b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/11-setup.json new file mode 100644 index 0000000..b8cc1fb --- /dev/null +++ b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/11-setup.json @@ -0,0 +1,24 @@ +{ + "action": "setup", + "ccr_sha256": "4a9668856389f986d4220bbd424bd6b353d86393bb931ebad293f726219d8754", + "coding_agents_denied": true, + "created": { + "email": "syn-journey@synthetic.invalid", + "groups": [], + "kv_path": "platform/workloads/net-kingdom/synthetic-journey/syn-journey", + "lldap_user": "syn-journey", + "pi_realm": "coulomb", + "pi_token_serial": "SYNJOURNEY01" + }, + "enrollment_confirmed_with_code": true, + "identity": "syn-journey", + "key_generated_by": "privacyidea", + "kv_version": 1, + "lldap_password_effective": true, + "phase": "done", + "source_ccr": "CCR-2026-0057", + "source_sha256": "a213b5f9fc6703dfc5ad619320a2fd083596ece7a2a1895f42829159bea42dde", + "status": "identity_prepared", + "values_recorded": false, + "workload_access": false +} diff --git a/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/12-verify.json b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/12-verify.json new file mode 100644 index 0000000..56dae36 --- /dev/null +++ b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/12-verify.json @@ -0,0 +1,91 @@ +{ + "action": "verify", + "ccr_sha256": "4a9668856389f986d4220bbd424bd6b353d86393bb931ebad293f726219d8754", + "coding_agents_denied": true, + "identity": "syn-journey", + "journey": { + "binds_on_path": { + "authelia_first_factor": "svc-authelia", + "keycape_otp": "svc-privacyidea", + "keycape_token": "svc-keycape" + }, + "claims": { + "assurance_level": "aal2", + "assurance_methods": [ + "pwd", + "otp" + ], + "assurance_source": "key-cape", + "kid": "key-1", + "preferred_username_matches": true, + "signature_verified": true + }, + "failed_step": null, + "finished_at": "2026-10-31T12:32:38Z", + "otp_time_step": 59781665, + "reason": null, + "result": "pass", + "run_id": "53251d54-80d1-4c23-bc6b-fd025e5d1a94", + "runner_sha256": "c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c", + "schema": "netkingdom.synthetic-journey.receipt/v1", + "started_at": "2026-10-31T12:32:35Z", + "steps": [ + { + "elapsed_ms": 117, + "name": "discovery", + "ok": true + }, + { + "elapsed_ms": 97, + "name": "keycape_authorize", + "ok": true + }, + { + "elapsed_ms": 771, + "name": "authelia_first_factor", + "ok": true + }, + { + "elapsed_ms": 326, + "name": "authelia_authorization", + "ok": true + }, + { + "elapsed_ms": 880, + "name": "keycape_callback", + "ok": true + }, + { + "elapsed_ms": 410, + "name": "keycape_otp", + "ok": true + }, + { + "elapsed_ms": 249, + "name": "keycape_token", + "ok": true + }, + { + "elapsed_ms": 99, + "name": "id_token", + "ok": true + } + ], + "target": { + "acr_values": "urn:netkingdom:aal2", + "client_id": "nk-synthetic-journey", + "host_mapped": [], + "issuer": "https://kc.coulomb.social", + "username": "syn-journey" + } + }, + "journey_passed": true, + "kv_is_current": true, + "kv_version": 1, + "phase": "done", + "source_ccr": "CCR-2026-0057", + "source_sha256": "a213b5f9fc6703dfc5ad619320a2fd083596ece7a2a1895f42829159bea42dde", + "status": "identity_verified", + "values_recorded": false, + "workload_access": false +} diff --git a/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md new file mode 100644 index 0000000..4aa098e --- /dev/null +++ b/docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md @@ -0,0 +1,43 @@ +# NK-WP-0056-T09: synthetic journey identity live (2026-10-31) + +Founder-run, attended. claude-code prepared the tooling and guided the run. Under CCR-2026-0057 +(railiance-platform `2ac9786`, sha256 `4a966885…8754`) and the accepted SecurityCase +`nk-synthetic-journey` (net-kingdom `d654f8a`): + +1. The KeyCape client `nk-synthetic-journey` (public, PKCE, MFA pinned) was registered with + `keycape-client.py apply` (check-config from the deployed `e2b5fa1` binary) and KeyCape restarted. +2. `lane.py setup` fenced coding agents from the KV path and created `syn-journey` in LLDAP with no + groups and a generated password. It enrolled TOTP token `SYNJOURNEY01` in realm `coulomb` with a + key generated by privacyIDEA, confirmed with a code, and wrote `PASSWORD` and `TOTP_SEED` to + `platform/workloads/net-kingdom/synthetic-journey/syn-journey` (KV v1, CAS=0). No workload role. +3. `lane.py verify`: the KV holds exactly the two fields, the fence is present, no workload role + exists, the KV password signs in to LLDAP, and **the synthetic journey passed against + `https://kc.coulomb.social`**: aal2, methods `pwd`+`otp`, signature verified against KeyCape's + JWKS, `preferred_username` matched. About 3 s end to end. + +| Receipt | Result | +| --- | --- | +| `00-setup-refused-absent.json` | refused, nothing changed (privacyIDEA token lookup by a user that did not resolve yet, or the pi-admin login) | +| `00-setup-refused-pi-admin-login.json` | refused at the pi-admin sign-in, nothing changed | +| `00-verify-before-setup.json` | expected failure: no KV yet | +| `01-setup.json` | stopped at `/token/init` after the user and KV v1 were written (see finding 1) | +| `10-teardown.json` | removed that partial state: token, user, KV | +| `11-setup.json` | `identity_prepared`, key generated by privacyIDEA, enrollment confirmed with a code | +| `12-verify.json` | `identity_verified`, journey `pass` (embedded, value-free) | + +All receipts were scanned for secret-shaped strings before commit. They hold identifiers, versions +and booleans only. + +## Findings + +1. **privacyIDEA silently replaces a supplied TOTP key for `pi-admin`.** An admin policy with + `action=*` (as `pi-admin-all-rights`) enables `totp_force_server_generate`, so `otpkey` is ignored + and the confirmation with a code from the supplied key fails (`ERR905`). This was reproduced on the + pinned 3.12.2 image. The lane now enrolls with `genkey=1` and takes the seed from the enrollment + response, in memory only. Any tool that enrolls a known seed as `pi-admin` has the same problem. +2. Listing privacyIDEA tokens by a user that does not resolve is an error, not an empty list. The + lane looks tokens up by serial. +3. Live KeyCape issues tokens to a user with no groups and no tenant mapping. The open point in the + runbook is answered. +4. Still open, recorded in CCR-2026-0057: KeyCape marks the token `principal_type: human`, and + KeyCape has no per-client group gate. diff --git a/tools/service-identity/declarations/lldap.yaml b/tools/service-identity/declarations/lldap.yaml index 63577f1..5235a80 100644 --- a/tools/service-identity/declarations/lldap.yaml +++ b/tools/service-identity/declarations/lldap.yaml @@ -94,7 +94,7 @@ identities: consumers: [{name: synthetic-journey, kind: service}] owner: "NetKingdom directory and User Engine owners" rotation: {path: "lane.py teardown then setup (CCR-2026-0057); password and TOTP seed together", coupling: [privacyidea]} - status: planned + status: active exceptions: - id: SI-X6 rules: [SI-3] diff --git a/workplans/NK-WP-0056-bounded-executor-path.md b/workplans/NK-WP-0056-bounded-executor-path.md index 27d85e6..99aec95 100644 --- a/workplans/NK-WP-0056-bounded-executor-path.md +++ b/workplans/NK-WP-0056-bounded-executor-path.md @@ -305,7 +305,7 @@ or clear the resolver cache, and the attended `--rotate-bind` MFA proof has the ```task id: NK-WP-0056-T09 -status: wait +status: done priority: medium assignee: claude-code needs_human: true @@ -331,6 +331,14 @@ reader), SecurityCase `security-cases/nk-synthetic-journey.yaml` (public PKCE cl Open, recorded: KeyCape marks every token `principal_type: human`; KeyCape has no per-client group gate. Waiting on: CCR approval, case acceptance, founder run of the runbook. +**Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md)): CCR-2026-0057 +approved; case accepted; client registered; founder-run `lane.py setup` and `verify` passed. The live journey +reached aal2 (`pwd`+`otp`, signature verified) against `kc.coulomb.social`. Two lane fixes on the way: privacyIDEA +tokens are looked up by serial, and enrollment uses a privacyIDEA-generated key, because `pi-admin`'s `action=*` +enables `totp_force_server_generate`, which silently replaced the supplied key (reproduced on 3.12.2). One +partial run was removed with `lane.py teardown`. Next: T10 needs the executor identity (T03) and a +`workload-kv-read` request for it. + ## Unattended live journey ```task