docs(security): map KeyCape rotation dependencies
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
parent
9ae4e24305
commit
c24d67bb85
2 changed files with 143 additions and 0 deletions
127
history/2026-08-23-keycape-exposure-dependency-map.md
Normal file
127
history/2026-08-23-keycape-exposure-dependency-map.md
Normal file
|
|
@ -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:
|
||||
<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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue