128 lines
8.8 KiB
Markdown
128 lines
8.8 KiB
Markdown
|
|
# 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.
|