Three gaps from NK-WP-0033, none of them the fault we set out to fix. Operator custody (platform-root-custody.md). This document named a password-safe entry that had never been created, so the 2026-08-23 rotation had nowhere to put the new LLDAP admin value and the outgoing one survived only in a browser entry that was overwritten on 2026-08-27. Decided: operators/<subsystem>/<account> in OpenBao for everything reachable once logged in, plus a short closed offline list for what you need when OpenBao is not — platform-root's own password, unseal shares, break-glass. Anything on that list in OpenBao would be circular; anything not on it does not belong in the packet. The rule that closes it: every rotation names, as a numbered step, where the outgoing and incoming values go. NK-WP-0033-T04 rotated four classes with no such step, and that is the root cause of the session that followed — not the four software defects it also surfaced. Secret disclosure by trust stage, tied to the existing S0-S6 table. A fingerprint may be spoken to confirm two parties hold the same credential; the whole value never may. Used ad hoc throughout 2026-08-27 and worth writing down. Attended procedures (new standard). reconcile-lldap-resolver-live.sh was approved, documented, revision-pinned and had never run; its first execution found four defects, two of which meant it could never have completed. verify-t06.sh passed throughout. An unexecuted procedure is unfinished work and must say so in its runbook header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3377672@bnt-lap001 Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
126 lines
6.4 KiB
Markdown
126 lines
6.4 KiB
Markdown
# KeyCape exposure: privacyIDEA resolver reconciliation
|
||
|
||
Incident: `KEYCAPE-EXPOSURE-20260823-01`
|
||
Workplan: `NK-WP-0033`
|
||
NetKingdom procedure: `sso-mfa/k8s/privacyidea/reconcile-lldap-resolver-live.sh` (repaired 2026-08-27 in `4a38511`; the previously pinned revision could not complete a run — see NK-WP-0033 T05)
|
||
Platform recovery contract: railiance-platform `453fed3`
|
||
Owner cutover receipt: State Hub message `45b236c8-052f-43d3-a472-44f8e9694da2`
|
||
Exercise status: exercised 2026-08-27 by Bernd Worsch — first run ever; four defects found and fixed (`4a38511`). See `attended-procedure-standard.md`.
|
||
|
||
This is the remaining attended provider-admin operation after the owner-reported
|
||
four-class cutover. It updates only privacyIDEA resolver `lldap-coulomb` so the
|
||
resolver uses the replacement LLDAP bind credential. It does not modify realms,
|
||
policies, tokens, KeyCape Secrets, or any other resolver.
|
||
|
||
No password, hash, token, Secret payload, or manifest belongs in this document,
|
||
State Hub, Git, chat, command arguments, or ordinary logs.
|
||
|
||
## Authority and pinning gate
|
||
|
||
The operator must record a private approval receipt containing, at minimum:
|
||
|
||
- incident `KEYCAPE-EXPOSURE-20260823-01`;
|
||
- the NetKingdom revision containing `reconcile-lldap-resolver-live.sh` and platform contract `453fed3`;
|
||
- the owner receipt `45b236c8-052f-43d3-a472-44f8e9694da2`;
|
||
- the exact start/end window, attended driver, and independent abort operator;
|
||
- confirmation that the replacement LLDAP credential is the provider-approved
|
||
value and that no exposed predecessor will be restored.
|
||
|
||
No live action is permitted if any revision, owner, cluster, or approval
|
||
identifier differs from the receipt.
|
||
|
||
## Preflight (metadata and health only)
|
||
|
||
Run from the approved operator workstation, with the cluster context and
|
||
provider endpoint already authorized. Do not render any Secret data.
|
||
|
||
1. Verify the approved checkout contains `reconcile-lldap-resolver-live.sh`
|
||
with mode `0755`; inspect its source, not live credential material.
|
||
2. Confirm LLDAP, privacyIDEA, KeyCape, Authelia, and
|
||
identity-provisioner are Ready using deployment/pod status fields only.
|
||
3. Confirm privacyIDEA availability via the documented unauthenticated
|
||
`/token/` probe (401/403 is expected) and KeyCape discovery health,
|
||
discarding response bodies. Do not use a command that prints a bearer token
|
||
or configuration response.
|
||
4. Confirm the approved window, driver, abort operator, provider custody, and
|
||
cleanup workspace are ready. Stop on any drift or missing owner.
|
||
|
||
## Apply (one attended operation)
|
||
|
||
1. From the approved checkout, run exactly:
|
||
|
||
```text
|
||
bash sso-mfa/k8s/privacyidea/reconcile-lldap-resolver-live.sh --apply
|
||
```
|
||
|
||
The helper securely prompts for pi-admin, replacement and predecessor
|
||
LLDAP passwords, and one approved MFA code; authenticates to privacyIDEA;
|
||
performs exactly one `POST /resolver/lldap-coulomb`; proves replacement
|
||
resolver lookup, privacyIDEA MFA, replacement LLDAP authentication, and
|
||
predecessor denial; rechecks readiness/health; securely cleans up; and
|
||
emits one sanitized PASS/FAIL receipt. It must not be combined with
|
||
`repair-realm-live.sh`, `bootstrap-realm.sh`, `creds-rotate.sh`, or any
|
||
full-bundle generator.
|
||
3. Stop immediately on any non-success response, timeout, unexpected endpoint,
|
||
or failed cleanup. Do not restore the exposed bundle or predecessor
|
||
credential.
|
||
|
||
## Postflight and predecessor denial
|
||
|
||
Record only status codes, readiness, timestamps, revision identifiers, and
|
||
boolean results.
|
||
|
||
1. Confirm privacyIDEA, LLDAP, KeyCape, Authelia, and identity-provisioner are
|
||
Ready again. Confirm the privacyIDEA health endpoint succeeds and the
|
||
resolver endpoint returns success without retaining its response body.
|
||
2. Exercise one approved KeyCape MFA path that requires the `coulomb` realm.
|
||
Record pass/fail only; never record the token or response body.
|
||
3. Using protected file inputs, prove an LDAP bind with the replacement value
|
||
succeeds and a bind with the exposed predecessor fails. The predecessor
|
||
test must be a boolean result and must not put the password in argv or
|
||
stdout. A failed predecessor bind is required evidence; do not retry it
|
||
against another provider.
|
||
4. Confirm the KeyCape owner’s existing four-class positive/negative receipt
|
||
remains associated with this resolver update. If any class lacks a receipt,
|
||
keep T05 open.
|
||
5. Securely remove the temporary workspace and record only cleanup success.
|
||
|
||
## Abort and rollback
|
||
|
||
Abort before mutation on revision drift, missing authority, unavailable health,
|
||
uncertain workspace cleanup, or any unsafe helper output. Abort forward after
|
||
mutation on a failed resolver response, failed readiness, failed replacement
|
||
MFA, or missing predecessor denial. The exposed bundle and every exposed
|
||
predecessor are never rollback material. Recovery after a partial write must
|
||
use a newly approved replacement value and revision, not a stale local bundle.
|
||
|
||
## Completion evidence
|
||
|
||
T03 may move to done only after the helper run and cleanup receipt are recorded
|
||
by the attended operator. T05 may move to done only after the resolver’s
|
||
replacement success, predecessor denial, owner cutover receipt, and all
|
||
residual limitations are recorded as sanitized evidence.
|
||
|
||
## Revision note — 2026-08-27
|
||
|
||
The pinned revision of `reconcile-lldap-resolver-live.sh` had never completed a
|
||
run. Four defects were found by running it and are fixed in `4a38511`; see
|
||
`NK-WP-0033` T05 for the full findings. Two change how this procedure is
|
||
invoked:
|
||
|
||
- `--predecessor-unavailable` — use when the exposed predecessor cannot be
|
||
produced. The denial bind is not attempted and the receipt records
|
||
`predecessor denial=NOT-PROVEN`. Do **not** type a placeholder at the
|
||
predecessor prompt instead: a wrong value also fails the bind, and the run
|
||
records it as a *passing* denial proof — a receipt asserting a test that never
|
||
ran.
|
||
- `--note TEXT` — operator context carried verbatim in the receipt line, so a
|
||
claim and its caveat travel together. One line, 200 characters, no
|
||
credentials.
|
||
|
||
`TIMEOUT`, `CACHE_TIMEOUT` and `SIZELIMIT` are now sent with the resolver body
|
||
(default 5 / 120 / 500, overridable via the matching `LDAP_*` environment
|
||
variables). Before this fix, `--apply` dropped them, because a resolver write
|
||
replaces the whole object. A resolver with them unset still resolves users, but
|
||
the WebUI refuses to save or test it — so a hand repair was silently reverted by
|
||
the next run.
|