Prepare bounded off-host privacyIDEA recovery drill
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
tegwick 2026-10-16 00:09:13 +02:00
parent c262470089
commit a1939a7d06
12 changed files with 549 additions and 23 deletions

View file

@ -136,9 +136,50 @@ 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 next human act is to run the existing custody helper in their own terminal
with `--expected-recipient` set to the public recipient in the discovery summary,
`--prompt --check-only`. Paste private material only into the hidden prompt.
This verifies the independently held key against the backup owner's declared
recipient and removes its temporary copy. It does not by itself prove that the
selected backup decrypts or includes the full matching identity recovery set.
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.
```bash
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.

View file

@ -1,8 +1,8 @@
{
"schema": "netkingdom.bootstrap-caller-inventory.v1",
"observed_at": "2026-10-15T20:54:09.856047+00:00",
"scope": "all current .sh/.py non-test files under sso-mfa/bootstrap and sso-mfa/k8s, Makefile, console, delivery copier and recovery transport tool plus two bounded provider metadata inventory helpers",
"entry_count": 89,
"observed_at": "2026-10-15T22:08:27.470032+00:00",
"scope": "all current .sh/.py non-test files under sso-mfa/bootstrap and sso-mfa/k8s, Makefile, console, delivery copier and recovery transport tool plus two bounded provider metadata inventory helpers plus bounded fixed-object ciphertext fetch and isolated PostgreSQL recovery check",
"entry_count": 91,
"entries": [
{
"path": "Makefile",
@ -431,7 +431,7 @@
},
{
"path": "tools/bootstrap-recovery/inventory-storage-share.py",
"sha256": "fe56454e5ca740395e6d6a2a0f8185abdac1d8ca6d0eedb31273db409dee4954",
"sha256": "673f9b17fb2f3fef6fcdb5c50a2bcca69e3df3157a9e5582a2d995c6b87640b5",
"classification": "read-only bounded owner backup metadata; existing caller/custody only; no age key or backup content read"
},
{
@ -448,6 +448,16 @@
"path": "tools/sync-user-engine-delivery-secret.sh",
"sha256": "7c0fb6b066e4ad8b103e75551beecd5de65a4bbb22570d95b42e5da6395e2dfc",
"classification": "retired entry point; refuses before credential/cluster access"
},
{
"path": "tools/bootstrap-recovery/fetch-privacyidea-backup.py",
"sha256": "233160e1206643a5cf7ad05141d733f0a75aff651190e27f77515a124ff0a59f",
"classification": "bounded off-host transfer / isolated database drill; actual recovery acceptance requires operator execution"
},
{
"path": "tools/bootstrap-recovery/check-privacyidea-database.py",
"sha256": "90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4",
"classification": "bounded off-host transfer / isolated database drill; actual recovery acceptance requires operator execution"
}
],
"source_inventory_complete_for_declared_scope": true,

View file

@ -0,0 +1,32 @@
{
"observed_at": "2026-10-15T22:08:27.470032+00:00",
"operator_reported_key_check_passed": true,
"private_key_received_by_agent": false,
"actual_ciphertext_decryption_verified": false,
"transfer_receipt": "privacyidea-transfer-r1.json",
"transfer_envelope_exit_code": 0,
"transfer_session_revoked_and_cleaned": true,
"source_tests": {
"passed": 23,
"command": "NK49_RUN_DISPOSABLE_POSTGRES_TEST=1 python3 -m pytest -q tests/test_privacyidea_offhost_recovery.py tests/test_storage_share_inventory.py tests/test_scaleway_backup_inventory.py",
"duration_seconds": 8.54,
"data": "synthetic database and fresh disposable age keys only",
"includes_native_postgres_restore_and_cleanup": true
},
"pending_human_command": "docs/bootstrap-recovery-ceremony.md: Prepared bounded privacyIDEA database drill",
"native_identity_recovery_proven": false,
"sources": [
{
"path": "tools/bootstrap-recovery/check-privacyidea-database.py",
"sha256": "90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4"
},
{
"path": "tools/bootstrap-recovery/fetch-privacyidea-backup.py",
"sha256": "233160e1206643a5cf7ad05141d733f0a75aff651190e27f77515a124ff0a59f"
},
{
"path": "tests/test_privacyidea_offhost_recovery.py",
"sha256": "ea977b0d3368fb14f6a7a252c24cc5cb9915022afa3dca61c80952e7489bd0e5"
}
]
}

View file

@ -1,5 +1,5 @@
{
"observed_at": "2026-10-15T21:58:20.278564+00:00",
"observed_at": "2026-10-15T22:08:27.470032+00:00",
"operator_reports_holding_railiance_backup_age_key": true,
"private_key_received_or_read": false,
"candidate_key_match_verified": false,
@ -10,8 +10,11 @@
"session_self_revocation_and_cleanup_confirmed": true,
"selected_candidate": "r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age",
"candidate_bytes": 89129309,
"candidate_contents_downloaded": false,
"bootstrap_identity_file_bundle_found_in_inspected_scope": false
"candidate_contents_downloaded": true,
"bootstrap_identity_file_bundle_found_in_inspected_scope": false,
"transfer_receipt": "privacyidea-transfer-r1.json",
"attended_transfer_envelope_exit_code": 0,
"transfer_session_self_revocation_and_cleanup_confirmed": true
},
"scaleway": {
"inventory": "scaleway-inventory-r1.json",
@ -26,7 +29,9 @@
"source_sha256": "09f349ae815339732db505d6896fb1ca384be7c66b40574750a3ba30d4d922e0",
"declared_default_public_recipient": "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz",
"operator_hidden_terminal_check_requested": true,
"scope": "A successful key match checks the declared public recipient, not actual ciphertext decryption or whole-stack recovery."
"scope": "A successful key match checks the declared public recipient, not actual ciphertext decryption or whole-stack recovery.",
"operator_reports_check_passed": true,
"operator_reports_temporary_key_removed": true
},
"source_tests": {
"passed": 13,

View file

@ -0,0 +1,19 @@
{
"schema": "netkingdom.privacyidea-offhost-transfer.v1",
"status": "ciphertext_fetched",
"started_at": "2026-10-15T22:07:08.615448+00:00",
"provider": "Storage Share",
"object": "r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age",
"age_keys_read": false,
"plaintext_created": false,
"production_writes": false,
"source_sha256": "233160e1206643a5cf7ad05141d733f0a75aff651190e27f77515a124ff0a59f",
"owner_accessor_sha256": "673f9b17fb2f3fef6fcdb5c50a2bcca69e3df3157a9e5582a2d995c6b87640b5",
"caller_auth_path": "auth/netkingdom/oidc/callback",
"caller_entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b",
"ciphertext_bytes": 89129309,
"ciphertext_sha256": "cc81da442755928171d2c51bce93eda7de4392a0a1aa9d0ffb83a25ba34e5ead",
"object_unchanged_during_transfer": true,
"ciphertext_path": "/tmp/nk49-offhost-j9dzl0ge/privacyidea.dump.age",
"finished_at": "2026-10-15T22:07:16.363645+00:00"
}

View file

@ -1,14 +1,14 @@
{
"workplan": "NK-WP-0049",
"observed_at": "2026-10-15T20:56:58.471680+00:00",
"observed_at": "2026-10-15T22:08:27.470032+00:00",
"status": "source_prepared_pending_owner_inputs_and_native_acceptance",
"backup_destinations_operator_reported": [
"Scaleway",
"Storage Share"
],
"exact_offhost_artifact_confirmed": false,
"exact_offhost_artifact_confirmed": true,
"independently_held_matching_key_confirmed": false,
"recovery_key_or_backup_contents_retrieved": false,
"recovery_key_or_backup_contents_retrieved": true,
"legacy_token_secret_present": true,
"legacy_trigger_secret_present": true,
"legacy_provider_revocation_proven": false,
@ -18,9 +18,9 @@
"verification": "source-verification.json",
"ceremony": "../../bootstrap-recovery-ceremony.md",
"outstanding": [
"Matching off-host identity data/key/config set is not located in inspected provider scopes; a recent PI database ciphertext is identified, not downloaded/decrypted",
"Independent age key availability/custody confirmation",
"Owner admitted isolated recovery target/procedure and native service continuity/cleanup evidence",
"Matching off-host identity data/key/config set remains unlocated in inspected scopes; selected existing PI database ciphertext is now fetched and digest-bound",
"Operator reports matching-recipient key check passed; actual ciphertext decryption/native database drill is prepared and pending operator hidden-key invocation",
"Full identity service recovery, token decryption, audit signature continuity and cleanup evidence",
"T02 current caller/provider source admission and exact legacy provider credential disposition; retain existing deferred-rotation boundary"
],
"operator_reports_holding_railiance_backup_age_key": true,
@ -29,5 +29,12 @@
"key_matches_selected_ciphertext_verified": false,
"offsite_discovery": "offsite-discovery-summary.json",
"privacyidea_database_ciphertext_identified": true,
"required_full_asset_coverage_confirmed": false
"required_full_asset_coverage_confirmed": false,
"operator_reports_recipient_check_passed": true,
"operator_reports_temporary_key_cleanup": true,
"private_key_received_by_agent": false,
"ciphertext_fetched": true,
"transfer_receipt": "privacyidea-transfer-r1.json",
"prepared_database_drill": "../../bootstrap-recovery-ceremony.md",
"age_private_key_retrieved_by_agent": false
}