net-kingdom/docs/bootstrap-recovery-ceremony.md
tegwick a1939a7d06
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Prepare bounded off-host privacyIDEA recovery drill
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-16 00:09:13 +02:00

12 KiB

NK-WP-0049 recovery ceremony and closure packet

Status: preparation; no live restore, credential read/grant, revocation, rotation, root retrieval or production bootstrap is authorized by this document.

What has been established

On 2026-10-15 the operator identified Scaleway and Storage Share as the off-host backup providers. This identifies destinations, not the exact recovery artifact, its freshness, its file coverage or independently held decryption key. Platform's current backup contracts distinguish Scaleway database/full archives from the Nextcloud secondary copy. An upload-only share is not a retrieval identity.

Read-only primary-cluster metadata is retained under docs/evidence/2026-10-15-bootstrap-recovery/. LLDAP, Authelia, KeyCape, privacyIDEA, identity-provisioner and User Engine each have one Ready replica. The three SQLite/key backup CronJobs report successful October 15 invocations. Their current commands target local PVCs; neither an external export nor complete off-host key/database coverage is established by those observations. The privacyIDEA command mentions its encryption key, but not its audit key pair. This command inspection is a coverage question, not proof that no other backup exists. The personal-token and trigger-admin Secrets still exist. Neither their validity nor provider-side revocation has been tested. A scan of 34 current Deployment/StatefulSet/DaemonSet/CronJob/Pod resources in sso, mfa and user-engine found no references to those two old Secrets; it still found the expected audit delivery reference in User Engine. Other namespaces, historic jobs, external consumers and issued provider credentials are outside that scan.

Assemble the exact owner packet before execution

Record only safe references and checksums. Keep credentials in their current owner custody and pass the decryption key through attended temporary unlock.

Required owner input Acceptance
Off-host artifact Provider, account/folder or object/version, backup timestamp, ciphertext SHA256 and independent retrieval receipt; do not substitute a new local export for the existing off-host backup.
Independent key Human confirms matching custody outside the servers and OpenBao. A successful decrypt using only the OpenBao escrow key does not prove independent recovery. Never send the key in chat.
Required assets Exact manifest of matching service env/config files, LLDAP and Authelia data, privacyIDEA database plus original encryption/audit keys, and KeyCape registry/signing material. Resolve assets held through other current owner lanes explicitly.
Recovery environment Named disposable target, pinned images available independently of the failed production registry, no production ingress, no production identity/client execution, bounded egress, private storage and exact cleanup scope.
Native proof Owner-specific database integrity, identity/group counts, decryption of retained token material, audit signature continuity, issuer/client/signing configuration preservation and admitted login/MFA checks. Distinguish an isolated restore from current production credential validity.
Current consumer source Reconcile the deployed revisions with the repaired caller/provider source. The October 14 reserved platform-group fix must be deployed and independently checked before claiming live enforcement.
Legacy credential disposition IAM owner identifies the exact historical personal-token/trigger principals, actual consumers and revocation method before any deletion. Deleting a Secret does not revoke an issued provider credential.

The operator subsequently reported holding the railiance-backup age key. Key material has not been received or read. Exact artifact/manifest selection and verification that this independently held key matches the selected ciphertext remain pending. The backup lane recipient and bootstrap keys/age.pub must not be assumed to be the same. Do not draft a blanket restore or grant approval while these remain unknown. Once fixed, bind the source revisions, target identities, allowed operations, recovery/cleanup sequence and maintenance window to the reviewed live packet. Existing eligibility or another client's approval is not this ceremony's grant.

Execute in stages

  1. Use the existing Platform/provider recovery lane to retrieve the identified ciphertext, verify its object/version and digest, and retain a safe transfer receipt. Do not request or expose the provider credential to the agent.
  2. The human unlocks the independently held age key in the existing custody helper. Validate it against the registered public recipient. No permanent key is installed in HOME, generated as a replacement, or copied into a bundle.
  3. Run the local transport/coverage check below. It contacts no cluster, keeps plaintext in a private temporary directory outside Git, refuses unsafe archives/private age keys, tests an unrelated disposable wrong key, reads the restored files back and removes that staging. It emits a closed non-secret receipt. This is one prerequisite, not whole-service recovery acceptance.
  4. Under the exact separately reviewed owner ceremony, restore the matching service data/key/config sets into the disposable environment and perform the native checks above. Never run the full production bootstrap to test source repairs. Unknown writes stop for reconciliation rather than automatic retry.
  5. Verify cleanup, preserve the original ciphertext and independent custody, and reconcile current callers/legacy credentials through their owners. Retain exact source/object/image/actor/time and positive/negative receipts.
  6. Close T02/T03 only after the recorded remaining requirements are satisfied. Update the workplan first, run statehub fix-consistency --repo net-kingdom, and verify the Hub projection. A transport fixture or generated evidence worksheet cannot mark this plan finished.

Prepared local transport check

The human invokes this through the existing attended custody helper after the exact artifact and required-file list are agreed. NETKINGDOM_CUSTODY_AGE_KEY_FILE is the temporary private file provided inside that envelope. The tool never reads an ambient HOME key. The receipt destination must be new.

python3 tools/bootstrap-recovery/verify-bundle.py \
  --bundle /private/recovery/fetched-ops-bundle.tar.age \
  --bundle-sha256 <independently-recorded-ciphertext-sha256> \
  --required-file lldap/secrets.env \
  --required-file privacyidea/secrets.env \
  --required-file privacyidea/pi.enc \
  --required-file privacyidea/private.pem \
  --required-file privacyidea/public.pem \
  --receipt /private/recovery/new-transport-receipt.json

This example file list is not an accepted whole-stack manifest. Add every agreed service file supported by the closed transport profile; database copies and other archive formats use their native owner restore tools. A successful receipt explicitly leaves independent custody, current service validity and native consumer recovery unproven. Off-host retrieval lineage and human custody confirmation must accompany it; the tool cannot infer where a local input came from. A failed check publishes no acceptance receipt.

Rotation boundaries

CUST-WP-0073-T05 remains founder-deferred under decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa. This work does not reopen that rotation, replace signing keys, change directory passwords or erase old backup keys. Existing historical exposure, provider revocation and coordinated rotation requirements stay explicit in their live owner records. If completion needs a protected native operation, prepare that exact operation and obtain its scoped disposition; never treat a source repair or successful backup job as permission.

Knowledge/source scope

Emergent Security 0.2.6, package digest 8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02: start/root cards withheld, orientation only. Current SECURITY-COMPANION, Platform backup/custody procedures, owner runtime contracts and the workplan were reviewed as sources. No execution authority is derived from the cards.

Off-host inventory return — 2026-10-15

Both bounded metadata inventories completed. Storage Share's inspected Backup/railiance-backups scope includes the current privacyIDEA database ciphertext and the October14 OpenBao snapshot. It did not reveal a matching bootstrap/identity-file bundle in the inspected folders. Scaleway's inspected railiance-platform-pg-backup/platform-pg/ tree contains database backup trees and an application archive subtree, with no identity-file bundle in this bounded scope. Other accounts/buckets/prefixes are unassessed. See the discovery summary. No object contents or age keys were read; the operator envelope exited0 and self-revoked. Existing scoped backup credentials were reused without changes; the Scaleway bootstrap API key was not used.

The operator subsequently reported the hidden-terminal check passed and the temporary key file was removed. This is operator-reported recipient matching; the private key was not provided to the agent. Actual decryption remains pending.

Prepared bounded privacyIDEA database drill — 2026-10-16

The identified existing off-host ciphertext was downloaded through the same attended Platform Backup-account procedure. The before/GET/after strong-ETag binding and exact byte count passed. Ciphertext SHA256: cc81da442755928171d2c51bce93eda7de4392a0a1aa9d0ffb83a25ba34e5ead. See privacyidea-transfer-r1.json in the October15 evidence folder. The owner envelope exited0 and self-revoked; no age key or plaintext was read by the fetch. Ciphertext is retained in the fresh private /tmp/nk49-offhost-j9dzl0ge/ directory.

The following exact human command performs only an isolated local database drill. It checks the transfer receipt/digest before opening the temporary key, checks the backup recipient, refuses an unrelated wrong key, decrypts to private temporary storage and restores into disposable PostgreSQL. Image postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70 is already cached locally (pg_restore16.14). There is no image pull, network, published port, host data mount, production connection or IAM/client execution. Database storage is a bounded temporary memory filesystem. Only fixed aggregate counts for token, resolver, and realm leave the database. The container, volumes and temporary plaintext are removed and cleanup is checked. The original ciphertext remains for reproducibility. This proves database recovery only; token decryption, audit signature continuity and full identity service recovery require matching key/config files and further native owner checks.

Run from the repository in the operator's own terminal. Paste the private key only into the hidden prompt, then share the success/error line or safe JSON receipt. The receipt destination must be new; do not overwrite a failed receipt.

cd /home/worsch/net-kingdom
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
  --expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
  --prompt -- python3 tools/bootstrap-recovery/check-privacyidea-database.py \
  --transfer-receipt docs/evidence/2026-10-15-bootstrap-recovery/privacyidea-transfer-r1.json \
  --receipt docs/evidence/2026-10-15-bootstrap-recovery/privacyidea-database-drill-r1.json

Source verification:23 focused checks passed, including a real local Docker custom-format dump, age encryption, temporary fixture-key unlock, wrong-key refusal, native restore/readback and cleanup. Those tests use synthetic data and fresh fixture keys. They are not evidence of actual backup decryption. If the real drill fails, review its closed receipt; do not expose PostgreSQL/age stderr or dump contents in chat. T02/T03 remain open.