diff --git a/history/2026-08-23-keycape-exposure-dependency-map.md b/history/2026-08-23-keycape-exposure-dependency-map.md new file mode 100644 index 0000000..ea16037 --- /dev/null +++ b/history/2026-08-23-keycape-exposure-dependency-map.md @@ -0,0 +1,127 @@ +# KeyCape exposure dependency and rotation map + +Date: 2026-08-23 +Incident: `KEYCAPE-EXPOSURE-20260823-01` +Workplan: `NK-WP-0033` +NetKingdom revision reviewed: `9ae4e24305f30c32059d6da5459171c8c07931c7` +Platform contract reviewed: railiance-platform `453fed3` + +This is value-safe planning evidence. It contains no credential value, encoded +Secret payload, reusable hash, private key, or token. It does not authorize a +live write. + +## Rulings + +1. The platform contract's single guarded KeyCape bundle is the correct + anti-staleness boundary. `keycape-config` combines the signing private key + and a full configuration document, so no independently generated full + Secret may be applied during the incident. +2. The LLDAP credential has four runtime consumers in this estate, not the two + named in the platform contract: Authelia, KeyCape, identity-provisioner, and + privacyIDEA's persisted `lldap-coulomb` resolver. +3. Updating `sso/lldap-secrets` and restarting LLDAP does not by itself change + the existing persistent `admin` account password. LLDAP documents + `LLDAP_LDAP_USER_PASS` as initial-account input. The provider write must use + a reviewed LLDAP password-change path; the Secret update is consumer and + desired-state reconciliation. +4. The privacyIDEA value embedded in KeyCape is an administrative session JWT + issued by `POST /auth`, not an individually managed privacyIDEA MFA token. + Current privacyIDEA documentation gives these JWTs an `exp` claim and a + default one-hour validity (policy may override it), but exposes no + individual session-JWT revocation endpoint. `/token/revoke` concerns user + MFA authenticators and must not be used for this credential. +5. Consequently, the platform contract cannot require both a 30-minute window + and immediate predecessor-token revocation without another approved + mechanism. T02 must choose either: + - issue the successor, stop using the predecessor, retain a protected + negative-test copy until its observed `exp`, and prove rejection after + expiry; or + - separately authorize rotation of privacyIDEA's JWT signing secret, with + the wider impact and rollback limits documented. Changing the `pi-admin` + password alone does not invalidate an already issued JWT. + +Primary provider references: + +- LLDAP configuration describes the admin password as initial-account input + and offers a force-reset recovery setting: + +- LLDAP scripting documentation describes its authentication and password + interfaces: + +- privacyIDEA authentication documentation describes `/auth` JWT issuance, + the `exp` claim, and default validity: + + +## Dependency map + +| Exposed class | Authoritative provider write | Consumer state that must change | Reload/roll | Predecessor-denial proof | +| --- | --- | --- | --- | --- | +| KeyCape RS256 signing key | KeyCape/railiance-platform guarded bundle apply using the KeyCape-approved signing strategy | `sso/keycape-config` `key.pem`; downstream JWKS caches | KeyCape and every declared downstream verifier | KeyCape-owned controlled old JWT rejection after explicit invalidation or overlap expiry | +| LLDAP bind/admin credential | Persistent LLDAP `admin` account through a reviewed official password-change path | `sso/lldap-secrets`; Authelia's `ldap_password`; KeyCape `lldap.bindPW`; identity-provisioner `LLDAP_ADMIN_PASSWORD`; privacyIDEA resolver `lldap-coulomb` `BINDPW` | Authelia, KeyCape, identity-provisioner; update/test privacyIDEA resolver; LLDAP only if its declared provider method requires it | Protected old-value LDAP bind fails; replacement lookup passes in Authelia, KeyCape, identity-provisioner, and privacyIDEA | +| Authelia KeyCape client credential | Authelia client registration hash in `sso/authelia-secrets` | KeyCape `authelia.clientSecret` in the one guarded bundle | Authelia and KeyCape | Protected predecessor produces `invalid_client`; replacement completes the authorization-code exchange | +| privacyIDEA administrative JWT | privacyIDEA `POST /auth` while predecessor remains valid | KeyCape `privacyidea.adminToken` in the one guarded bundle; any separately maintained `keycape-pi-token` must be reconciled or retired as a duplicate source | KeyCape | Replacement performs the required MFA API call; predecessor rejects only after observed expiry unless global JWT-signing-secret invalidation is separately approved | + +## Exact NetKingdom-owned sequence + +These steps describe the required shape; they are not a live GO. + +### Prepare without provider writes + +1. Pin the reviewed non-secret KeyCape client configuration and all three + repositories named by the platform contract. +2. Create a mode-`0700` private workspace with a cleanup trap. Receive each + replacement through protected input into a separate mode-`0600` file. +3. Produce the Authelia bcrypt client hash from protected stdin/file, never a + command argument. Do not record the hash as evidence. +4. Build field-specific replacements for `authelia-secrets` and + `lldap-secrets`. Preserve their unrelated fields from an owner-controlled + input, not from captured agent output and not from a stale bootstrap bundle. +5. Build exactly one complete KeyCape configuration and signing-key pair from + the pinned non-secret client source and the replacement files. +6. Prepare the privacyIDEA resolver update for `lldap-coulomb` with its complete + non-secret resolver settings and the replacement bind value. Do not run the + broad realm bootstrap, which also rewrites realms and policies. + +### Bounded cutover + +1. Guard all prepared writes on the approved Kubernetes UIDs/resource + versions and Git revisions. +2. Install the field-specific Authelia Secret changes without restarting it. +3. Change the persistent LLDAP `admin` password using the reviewed provider + path. Do not assume a Deployment restart applies the new value. +4. Reconcile `sso/lldap-secrets`, update privacyIDEA resolver + `lldap-coulomb`, and restart identity-provisioner. The resolver update must + be a dedicated `/resolver/lldap-coulomb` operation, not full realm repair. +5. Apply the single guarded `sso/keycape-config` bundle without rendering a + manifest, then roll Authelia and KeyCape. The Authelia Secret update and the + KeyCape bundle must contain the same LLDAP and client-credential revisions. +6. Prove replacement LLDAP access through all four consumers, replacement + Authelia client authentication, and replacement privacyIDEA access. +7. Prove old LLDAP and Authelia credentials fail using protected inputs. + Handle the old privacyIDEA JWT according to the corrected T02 decision. + +After any provider write, abort is forward-only: finish or repair with the new +revision. The exposed bundle and exposed predecessors are never rollback +material. + +## Existing helper assessment + +| Helper | Assessment for this incident | +| --- | --- | +| `sso-mfa/bootstrap/creds-rotate.sh` | Prohibited. It prints replacements; its LLDAP branch omits identity-provisioner and privacyIDEA; its LLDAP provider change is manual; and its signing branch deliberately invalidates immediately. | +| `sso-mfa/k8s/authelia/create-secrets.sh` | Not suitable as the cutover writer. It reconstructs the whole Secret from local bootstrap files, may generate unrelated signing material, and passes plaintext/hash values in command arguments. Its field mapping is useful as design evidence only. | +| `sso-mfa/k8s/keycape/create-secrets.sh` | Not suitable. It reconstructs the full bundle from local files, can generate a new key implicitly, and its inline client list can lag the live reviewed source. | +| `sso-mfa/k8s/keycape/refresh-pi-token-live.sh` | Not suitable. It reads the exposed live KeyCape payload, rebuilds the full Secret, and has no predecessor-expiry/revocation proof. | +| `sso-mfa/k8s/privacyidea/repair-realm-live.sh` | Too broad. It updates the resolver but also reapplies realm, default-realm, and policy state. A dedicated resolver-only path is required. | +| `sso-mfa/k8s/lldap/break-glass.sh` password mutation | Not accepted as the provider path. It passes the password in process arguments and assumes a GraphQL cleartext mutation that is not the documented general LLDAP password interface. | + +## Remaining gates + +- KeyCape must publish and acknowledge its signing/JWKS/cache strategy. +- Railiance-platform must correct the LLDAP consumer list and the privacyIDEA + predecessor disposition in its owner contract. +- NetKingdom must implement and review provider-specific, file-input helpers + before T03 can complete. They must have dry-run/static tests proving that + values cannot reach argv or stdout. +- A named operator window, driver, abort operator, provider authority, and + protected verification method are still required before any live action. diff --git a/workplans/NK-WP-0033-keycape-secret-exposure-rotation.md b/workplans/NK-WP-0033-keycape-secret-exposure-rotation.md index 8149a05..af81bdc 100644 --- a/workplans/NK-WP-0033-keycape-secret-exposure-rotation.md +++ b/workplans/NK-WP-0033-keycape-secret-exposure-rotation.md @@ -72,6 +72,16 @@ immediate token invalidation and verifier refresh. Evidence was routed in `2b24ccc2-c031-453e-ac70-52f0aaa127d5` and `9277d962-b511-4da7-9fc6-2c79bac448c1`. +Railiance-platform published its forward-only owner contract at revision +`453fed3`. NetKingdom review is recorded in +`history/2026-08-23-keycape-exposure-dependency-map.md`. The review found that +the LLDAP credential also feeds identity-provisioner and privacyIDEA's +persisted LDAP resolver. It also found that privacyIDEA `/auth` issues an +expiring administrative session JWT with no individual revocation operation; +`/token/revoke` applies to MFA authenticators, not that JWT. T02 therefore +remains in progress until the contract chooses expiry-based predecessor denial +or separately authorizes wider JWT-signing-secret invalidation. + Warden routing has no concrete entries for these four KeyCape lanes. Catalog repair was requested in `e5794329-cda5-4a6f-8234-a6eaa1fc318c`; generic or unrelated routes do not authorize execution. @@ -90,6 +100,12 @@ replacement material through hidden input or mode-0600 files, updates every provider and consumer in the agreed order, never reconstructs the bundle from stale local inputs, and retains no plaintext artifact after verification. +Provider-specific helpers must cover field-safe updates for `lldap-secrets` +and `authelia-secrets`, the persistent LLDAP admin password, the +identity-provisioner restart, privacyIDEA resolver `lldap-coulomb`, the +Authelia client hash, and the privacyIDEA JWT expiry disposition. Static or +dry-run tests must prove replacements do not enter argv or stdout. + Do not use `sso-mfa/bootstrap/creds-rotate.sh` through an agent as currently written: it prints generated replacement values and its signing-key path causes immediate invalidation. Do not use a helper that reads the live Secret