net-kingdom/docs/keycape-exposure-resolver-reconciliation.md
tegwick 4dffe2fe7a
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix: preserve resolver configuration during attended password cutover
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
2026-10-14 01:52:37 +02:00

7.8 KiB

KeyCape exposure: privacyIDEA resolver reconciliation

Incident: KEYCAPE-EXPOSURE-20260823-01 Historical workplan: NK-WP-0033; source maintenance: NK-WP-0049-T02 Exercise status: core helper exercised 2026-10-14 by custodian-codex against local disposable privacyIDEA 3.12.2 and LLDAP, including actual LDAP bind, password update, preserved configuration, exact user lookup and HOTP. Terminal handling and failure paths have automated tests. The complete new attended procedure has not run against the production cluster or KeyCape. Evidence: Custodian docs/evidence/2026-10-14-netkingdom-security/resolver-cutover.json.

Both sso-mfa/k8s/privacyidea/reconcile-lldap-resolver-live.sh and update-lldap-resolver-live.sh delegate to resolver_cutover.py. The update helper changes only an existing lldap-coulomb resolver's bind password; the reconcile helper additionally checks cluster readiness, discovery, exact user resolution, token-backed MFA and the supplied predecessor's HTTP denial. Neither helper bootstraps a missing resolver or realm, repairs missing tuning, or changes policies, tokens, directory credentials or Kubernetes resources.

Authority and target pins

The historical platform recovery contract is railiance-platform 453fed3; owner cutover receipt is State Hub message 45b236c8-052f-43d3-a472-44f8e9694da2. These identify historical evidence, not a fresh live grant. Founder-deferred exposure rotation remains deferred.

Before any live run, the existing owner/supervisor path must bind a private approval record to the current NetKingdom source revision and dependencies, incident, provider-approved replacement, owner receipt, exact start/end window, attended driver, independent abort operator and recovery plan. Confirm predecessor provenance separately. Do not restore an exposed predecessor or bundle. A preview digest and TTY are technical preconditions, not authorization.

Reconcile requires an explicit absolute kubeconfig, context, expected API server and kube-system namespace UID from the reviewed owner packet. It checks the server and UID before workload reads. It never reads Kubernetes Secrets. The five existing deployments must each have the current generation observed and exactly one desired, actual, updated, ready and available replica, without a deletion timestamp. No ambient default context is accepted.

Default service origins are https://pink.coulomb.social, https://lldap.coulomb.social and https://kc.coulomb.social. Explicit literal loopback URLs with ports are supported for approved forwards or fixtures. Environment URL/tuning overrides and the old --note option are removed. KeyCape discovery must identify the canonical https://kc.coulomb.social issuer and its own authorization, token and JWKS endpoints, even through a forward. The shared transport disables proxies and redirects and bounds strict JSON responses; each extra service profile permits only its one required route and never receives the privacyIDEA authorization token.

Preview, apply and check

Use an attended terminal from the reviewed checkout. These are command templates; replace public placeholders with the approved target values. No secret belongs in argv, environment, Git, State Hub, chat or ordinary logs.

bash sso-mfa/k8s/privacyidea/reconcile-lldap-resolver-live.sh --preview \
  --kubeconfig /absolute/approved/kubeconfig --context APPROVED_CONTEXT \
  --server https://APPROVED_SERVER:6443 --cluster-uid APPROVED_NAMESPACE_UID

Preview prompts only for pi-admin, authenticates with an admin-role check, validates the existing resolver/core/tuning/realm, and returns a public-state snapshot_sha256. It performs no configuration write. It creates an auth session and can produce provider audit events. The digest excludes the bind password and binds the target settings and observed non-secret configuration. It is not a credential fingerprint.

Run the same command with --apply --expected-digest REVIEWED_DIGEST in place of --preview, retaining all target and user options. The helper prompts silently for pi-admin, the approved replacement and the actual predecessor. Inputs stay in Python memory; no temporary secret files are created. Inherited shell tracing is disabled before input handling. A fresh OTP/PIN is requested only after configuration and exact user lookup succeed. --user defaults to platform-root.

Before mutation the helper authenticates the replacement to LLDAP and uses privacyIDEA /resolver/test with the candidate password to prove an actual LDAP bind. It tests supplied predecessor denial at LLDAP HTTP login and rechecks the observed configuration. Apply sends exactly one resolver update containing only type and BINDPW. Native privacyIDEA 3.12.2 preserves omitted fields. Readback must preserve all other resolver data and every realm; a native resolver test then proves the stored bind works. The API has no compare-and-swap: an exclusive owner window remains required, and ambiguous writes are never retried.

--check replaces --preview to perform the proofs without a configuration write; it needs no digest. Check proves the stored bind works, not that its censored value equals the supplied replacement. Authentication, audit and OTP counter effects still occur. It is not an effect-free operation.

The smaller update-lldap-resolver-live.sh supports the same preview/apply/check sequence without cluster flags. It proves candidate and stored binds plus exact configuration preservation, but does not claim MFA, readiness or predecessor acceptance. It must not substitute for full incident acceptance.

Receipts and recovery

Receipts contain fixed phase names, booleans, write counts and a public-state digest; no raw responses, exceptions or free-form notes. configured means the update helper completed its apply; checked means its no-write proof completed. Reconcile returns verified only when all helper proofs pass. Passing a wrong arbitrary password also produces denial: supplied_predecessor_denied is not proof of predecessor provenance. The owner must match the input to custody.

Use --predecessor-unavailable honestly when it cannot be produced. Reconcile then returns incomplete, exit 2, even if the other proofs pass; never type a placeholder to obtain a passing receipt. Failures and handled interruptions return unverified, exit 1, with write_attempted showing whether a partial change may exist. Exit 0 still leaves incident_acceptance: unverified: the separate owner receipt and actual KeyCape MFA path are outside this helper.

Stop on drift, wrong target, failed health/readiness or any unverified result. After an attempted write, reconcile observed state through the approved owner path before another attempt. Recover forward with an approved replacement; no automatic rollback to an exposed predecessor. Record only sanitized evidence, source revision, target references and operator outcome. NK-WP-0049 T03 remains the owner-native recovery/acceptance gate; this source repair does not close it.

Historical receipts and corrections

The old --check --predecessor-unavailable printed PASS on 2026-09-23 when run by Bernd Worsch from railiance01 checkout 6096c395 (receipt in NK-WP-0033). That remains historical check evidence, with predecessor denial unproven; it cannot establish the new revision's acceptance. Historical apply last ran on 2026-08-27 before fixes in 4a38511.

The August account attributed missing tuning to whole-object replacement. Native privacyIDEA 3.12.2 and source inspection corrected that interpretation on 2026-10-13: omitted fields are preserved, while sending explicit defaults can overwrite custom tuning. The October helper therefore sends only the bind password and type. Historic claims, default-overwriting commands, temporary-file handling and raw --note receipts are superseded by this procedure.