net-kingdom/history/2026-08-23-keycape-exposure-dependency-map.md

146 lines
9.7 KiB
Markdown
Raw Permalink Normal View History

# 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:
<https://github.com/lldap/lldap/blob/main/lldap_config.docker_template.toml>
- LLDAP scripting documentation describes its authentication and password
interfaces:
<https://github.com/lldap/lldap/blob/main/docs/scripting.md>
- privacyIDEA authentication documentation describes `/auth` JWT issuance,
the `exp` claim, and default validity:
<https://privacyidea.readthedocs.io/en/stable/modules/api/auth.html>
## 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.
NetKingdom added
`sso-mfa/k8s/privacyidea/update-lldap-resolver-live.sh` as the bounded
resolver-only operation. It requires explicit `--apply` plus an interactive
terminal, passes password files by pathname to a child process, updates only
`lldap-coulomb`, and emits only a boolean result. It has not been run.
## Owner-reported cutover outcome
KeyCape reported on 2026-08-23 that its governed recovery path completed the
four-class replacement, including privacyIDEA signing-secret replacement, and
that all four deployments were Ready with positive and negative checks passing.
No credential values or live Secret payloads were returned to NetKingdom.
The remaining NetKingdom action is source reconciliation: Authelia 4.38 does
not support `*_FILE` overrides for OIDC client list entries. The ConfigMap must
therefore use the supported template filter reading the Secret-mounted bcrypt
verifier, with `X_AUTHELIA_CONFIG_FILTERS=template` enabled in the Deployment.