Inventory off-host recovery assets through existing bounded backup routes
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-15 23:58:41 +02:00
parent d9557ba90e
commit c262470089
12 changed files with 1069 additions and 6 deletions

View file

@ -41,7 +41,11 @@ owner custody and pass the decryption key through attended temporary unlock.
| 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 current independent key availability and exact artifact/manifest are pending.
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.
@ -116,3 +120,25 @@ Emergent Security 0.2.6, package digest
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](evidence/2026-10-15-bootstrap-recovery/offsite-discovery-summary.json).
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.

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",
"entry_count": 87,
"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,
"entries": [
{
"path": "Makefile",
@ -424,6 +424,16 @@
"sha256": "7efe14c0d71f2c846434d19f8d05702d69d3c40a617a700b08fe13b3764d0650",
"classification": "diagnostic; structural observations do not prove recovery or revoke credentials"
},
{
"path": "tools/bootstrap-recovery/inventory-scaleway.py",
"sha256": "a219df205708fa4ed44b763a418585d63c820c25434bc0fd14e805eb5c2f79d3",
"classification": "read-only bounded owner backup metadata; existing caller/custody only; no age key or backup content read"
},
{
"path": "tools/bootstrap-recovery/inventory-storage-share.py",
"sha256": "fe56454e5ca740395e6d6a2a0f8185abdac1d8ca6d0eedb31273db409dee4954",
"classification": "read-only bounded owner backup metadata; existing caller/custody only; no age key or backup content read"
},
{
"path": "tools/bootstrap-recovery/verify-bundle.py",
"sha256": "885b05a2011e711228e42e316bda70bbe0d97d481383d61aa8469d0f10452370",

View file

@ -0,0 +1,37 @@
{
"observed_at": "2026-10-15T21:58:20.278564+00:00",
"operator_reports_holding_railiance_backup_age_key": true,
"private_key_received_or_read": false,
"candidate_key_match_verified": false,
"storage_share": {
"inventory": "storage-share-inventory-r1.json",
"status": "metadata_inventory_complete",
"attended_envelope_exit_code": 0,
"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
},
"scaleway": {
"inventory": "scaleway-inventory-r1.json",
"status": "metadata_inventory_complete",
"bucket": "railiance-platform-pg-backup",
"scope": "platform-pg/ and at most three child levels; WAL/base object trees and other buckets/prefixes not enumerated",
"bootstrap_identity_file_bundle_found_in_inspected_scope": false,
"bootstrap_api_key_used": false
},
"key_check": {
"source": "railiance-platform/lib/railiance-backup-common.sh",
"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."
},
"source_tests": {
"passed": 13,
"command": "python3 -m pytest -q tests/test_scaleway_backup_inventory.py tests/test_storage_share_inventory.py"
},
"native_recovery_proven": false,
"production_mutations_performed": false
}

View file

@ -18,9 +18,16 @@
"verification": "source-verification.json",
"ceremony": "../../bootstrap-recovery-ceremony.md",
"outstanding": [
"Exact off-host artifact/version/digest and complete matching data/key/config coverage",
"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",
"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,
"operator_report_recorded_at": "2026-10-15T21:52:22.549430+00:00",
"key_material_received": false,
"key_matches_selected_ciphertext_verified": false,
"offsite_discovery": "offsite-discovery-summary.json",
"privacyidea_database_ciphertext_identified": true,
"required_full_asset_coverage_confirmed": false
}

View file

@ -0,0 +1,93 @@
{
"schema": "netkingdom.scaleway-backup-inventory.v1",
"status": "metadata_inventory_complete",
"started_at": "2026-10-15T21:54:28.925951+00:00",
"read_only": true,
"provider": "Scaleway",
"bucket": "railiance-platform-pg-backup",
"prefix": "platform-pg/",
"source_sha256": "a219df205708fa4ed44b763a418585d63c820c25434bc0fd14e805eb5c2f79d3",
"bootstrap_api_key_used": false,
"age_keys_read": false,
"object_contents_downloaded": false,
"secret_values_exported": false,
"scope": "Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed",
"prefixes": {
"platform-pg/": {
"directories": [
"platform-pg/application-archives/",
"platform-pg/apps-pg/",
"platform-pg/forgejo-db/",
"platform-pg/platform-pg-2/",
"platform-pg/platform-pg/"
],
"files": []
},
"platform-pg/application-archives/": {
"directories": [
"platform-pg/application-archives/email-connect/"
],
"files": []
},
"platform-pg/apps-pg/": {
"directories": [
"platform-pg/apps-pg/apps-pg/"
],
"files": []
},
"platform-pg/forgejo-db/": {
"directories": [
"platform-pg/forgejo-db/forgejo-db/"
],
"files": []
},
"platform-pg/platform-pg-2/": {
"directories": [
"platform-pg/platform-pg-2/platform-pg-2/"
],
"files": []
},
"platform-pg/platform-pg/": {
"directories": [
"platform-pg/platform-pg/base/",
"platform-pg/platform-pg/wals/"
],
"files": []
},
"platform-pg/application-archives/email-connect/": {
"directories": [
"platform-pg/application-archives/email-connect/2026/"
],
"files": []
},
"platform-pg/apps-pg/apps-pg/": {
"directories": [
"platform-pg/apps-pg/apps-pg/base/",
"platform-pg/apps-pg/apps-pg/wals/"
],
"files": []
},
"platform-pg/forgejo-db/forgejo-db/": {
"directories": [
"platform-pg/forgejo-db/forgejo-db/base/",
"platform-pg/forgejo-db/forgejo-db/wals/"
],
"files": []
},
"platform-pg/platform-pg-2/platform-pg-2/": {
"directories": [
"platform-pg/platform-pg-2/platform-pg-2/base/",
"platform-pg/platform-pg-2/platform-pg-2/wals/"
],
"files": []
},
"platform-pg/application-archives/email-connect/2026/": {
"directories": [
"platform-pg/application-archives/email-connect/2026/10/"
],
"files": []
}
},
"runtime_delivery_uid": "30c4f6fc-8eb7-4cdc-b692-0f74c4a5b3ce",
"finished_at": "2026-10-15T21:54:39.109262+00:00"
}

View file

@ -0,0 +1,562 @@
{
"schema": "netkingdom.storage-share-inventory.v1",
"status": "metadata_inventory_complete",
"started_at": "2026-10-15T21:52:20.452797+00:00",
"read_only": true,
"age_keys_read": false,
"backup_contents_downloaded": false,
"secret_values_exported": false,
"source_sha256": "fe56454e5ca740395e6d6a2a0f8185abdac1d8ca6d0eedb31273db409dee4954",
"owner_sources": [
{
"path": "/home/worsch/railiance-platform/scripts/state_hub_preflight_lane.py",
"sha256": "8bf63beda028de7c2375084afb7c22fb00a731c54ebf0b3ba369a699d920e0f5"
},
{
"path": "/home/worsch/railiance-platform/scripts/migrate_nextcloud_backup_account.py",
"sha256": "d3285f403f3c947f75c3288d989152e59ab9a8f5542bcf901bd0a06b0f3e050c"
}
],
"root": [
{
"name": "core-apps-pg",
"directory": true,
"bytes": null
},
{
"name": "core-gitea-db",
"directory": true,
"bytes": null
},
{
"name": "core-net-kingdom-pg",
"directory": true,
"bytes": null
},
{
"name": "core-state-hub-db",
"directory": true,
"bytes": null
},
{
"name": "forgejo",
"directory": true,
"bytes": null
},
{
"name": "openbao",
"directory": true,
"bytes": null
},
{
"name": "r01-forgejo-db",
"directory": true,
"bytes": null
},
{
"name": "r01-net-kingdom-pg",
"directory": true,
"bytes": null
},
{
"name": "r01-platform-pg",
"directory": true,
"bytes": null
},
{
"name": "r01-state-hub-db",
"directory": true,
"bytes": null
},
{
"name": "r01-user-engine-pg",
"directory": true,
"bytes": null
}
],
"folders": {
"core-apps-pg": [],
"core-gitea-db": [],
"core-net-kingdom-pg": [],
"core-state-hub-db": [],
"openbao": [
{
"name": "openbao-raft-snapshot-RPF-WP-0015-T03-REBOOT-20261014-01-20261014T181053Z.snap.age",
"directory": false,
"bytes": 375618
}
],
"r01-forgejo-db": [
{
"name": "forgejo-db-forgejo-20260906T023000Z.dump.age",
"directory": false,
"bytes": 7515593
},
{
"name": "forgejo-db-forgejo-20260907T023000Z.dump.age",
"directory": false,
"bytes": 7320543
},
{
"name": "forgejo-db-forgejo-20260908T023000Z.dump.age",
"directory": false,
"bytes": 7413040
},
{
"name": "forgejo-db-forgejo-20260909T023000Z.dump.age",
"directory": false,
"bytes": 7663338
},
{
"name": "forgejo-db-forgejo-20260910T023000Z.dump.age",
"directory": false,
"bytes": 8079045
},
{
"name": "forgejo-db-forgejo-20260911T023000Z.dump.age",
"directory": false,
"bytes": 8404583
},
{
"name": "forgejo-db-forgejo-20260912T023000Z.dump.age",
"directory": false,
"bytes": 8631188
},
{
"name": "forgejo-db-forgejo-20260913T023000Z.dump.age",
"directory": false,
"bytes": 8472109
},
{
"name": "forgejo-db-forgejo-20260914T023000Z.dump.age",
"directory": false,
"bytes": 8523610
},
{
"name": "forgejo-db-forgejo-20261005T023000Z.dump.age",
"directory": false,
"bytes": 11745885
},
{
"name": "forgejo-db-forgejo-20261006T023000Z.dump.age",
"directory": false,
"bytes": 11481436
},
{
"name": "forgejo-db-forgejo-20261007T023000Z.dump.age",
"directory": false,
"bytes": 11693800
},
{
"name": "forgejo-db-forgejo-20261008T023000Z.dump.age",
"directory": false,
"bytes": 11784032
},
{
"name": "forgejo-db-forgejo-20261009T023003Z.dump.age",
"directory": false,
"bytes": 12210534
},
{
"name": "forgejo-db-forgejo-20261010T023000Z.dump.age",
"directory": false,
"bytes": 12378726
},
{
"name": "forgejo-db-forgejo-20261011T023000Z.dump.age",
"directory": false,
"bytes": 12369466
},
{
"name": "forgejo-db-forgejo-20261012T023000Z.dump.age",
"directory": false,
"bytes": 12701177
},
{
"name": "forgejo-db-forgejo-20261013T023000Z.dump.age",
"directory": false,
"bytes": 12761857
},
{
"name": "forgejo-db-forgejo-20261014T023000Z.dump.age",
"directory": false,
"bytes": 13110850
},
{
"name": "forgejo-db-forgejo-20261015T023000Z.dump.age",
"directory": false,
"bytes": 13349242
},
{
"name": "forgejo-db-forgejo-weekly-20260906T023000Z.dump.age",
"directory": false,
"bytes": 7515593
},
{
"name": "forgejo-db-forgejo-weekly-20260913T023000Z.dump.age",
"directory": false,
"bytes": 8472109
},
{
"name": "forgejo-db-forgejo-weekly-20261011T023000Z.dump.age",
"directory": false,
"bytes": 12369466
}
],
"r01-net-kingdom-pg": [
{
"name": "net-kingdom-pg-interhub-20260906T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260907T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260908T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260909T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260910T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260911T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260912T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260913T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20260914T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261005T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261006T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261007T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261008T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261009T023003Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261010T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261011T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261012T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261013T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261014T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-20261015T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-weekly-20260906T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-weekly-20260913T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-interhub-weekly-20261011T023000Z.dump.age",
"directory": false,
"bytes": 215541
},
{
"name": "net-kingdom-pg-privacyidea_db-20260906T023000Z.dump.age",
"directory": false,
"bytes": 271941
},
{
"name": "net-kingdom-pg-privacyidea_db-20260907T023000Z.dump.age",
"directory": false,
"bytes": 272271
},
{
"name": "net-kingdom-pg-privacyidea_db-20260908T023000Z.dump.age",
"directory": false,
"bytes": 272599
},
{
"name": "net-kingdom-pg-privacyidea_db-20260909T023000Z.dump.age",
"directory": false,
"bytes": 274660
},
{
"name": "net-kingdom-pg-privacyidea_db-20260910T023000Z.dump.age",
"directory": false,
"bytes": 274660
},
{
"name": "net-kingdom-pg-privacyidea_db-20260911T023000Z.dump.age",
"directory": false,
"bytes": 274996
},
{
"name": "net-kingdom-pg-privacyidea_db-20260912T023000Z.dump.age",
"directory": false,
"bytes": 276353
},
{
"name": "net-kingdom-pg-privacyidea_db-20260913T023000Z.dump.age",
"directory": false,
"bytes": 277468
},
{
"name": "net-kingdom-pg-privacyidea_db-20260914T023000Z.dump.age",
"directory": false,
"bytes": 1008342
},
{
"name": "net-kingdom-pg-privacyidea_db-20261005T023000Z.dump.age",
"directory": false,
"bytes": 58987550
},
{
"name": "net-kingdom-pg-privacyidea_db-20261006T023000Z.dump.age",
"directory": false,
"bytes": 61979707
},
{
"name": "net-kingdom-pg-privacyidea_db-20261007T023000Z.dump.age",
"directory": false,
"bytes": 64976206
},
{
"name": "net-kingdom-pg-privacyidea_db-20261008T023000Z.dump.age",
"directory": false,
"bytes": 68001818
},
{
"name": "net-kingdom-pg-privacyidea_db-20261009T023003Z.dump.age",
"directory": false,
"bytes": 71016444
},
{
"name": "net-kingdom-pg-privacyidea_db-20261010T023000Z.dump.age",
"directory": false,
"bytes": 74006618
},
{
"name": "net-kingdom-pg-privacyidea_db-20261011T023000Z.dump.age",
"directory": false,
"bytes": 77020445
},
{
"name": "net-kingdom-pg-privacyidea_db-20261012T023000Z.dump.age",
"directory": false,
"bytes": 80037505
},
{
"name": "net-kingdom-pg-privacyidea_db-20261013T023000Z.dump.age",
"directory": false,
"bytes": 83066939
},
{
"name": "net-kingdom-pg-privacyidea_db-20261014T023000Z.dump.age",
"directory": false,
"bytes": 86103429
},
{
"name": "net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age",
"directory": false,
"bytes": 89129309
},
{
"name": "net-kingdom-pg-privacyidea_db-weekly-20260906T023000Z.dump.age",
"directory": false,
"bytes": 271941
},
{
"name": "net-kingdom-pg-privacyidea_db-weekly-20260913T023000Z.dump.age",
"directory": false,
"bytes": 277468
},
{
"name": "net-kingdom-pg-privacyidea_db-weekly-20261011T023000Z.dump.age",
"directory": false,
"bytes": 77020445
}
],
"r01-platform-pg": [],
"r01-state-hub-db": [
{
"name": "state-hub-db-state_hub-20260906T023000Z.dump.age",
"directory": false,
"bytes": 6924847
},
{
"name": "state-hub-db-state_hub-20260907T023000Z.dump.age",
"directory": false,
"bytes": 7141118
},
{
"name": "state-hub-db-state_hub-20260908T023000Z.dump.age",
"directory": false,
"bytes": 7180575
},
{
"name": "state-hub-db-state_hub-20260909T023000Z.dump.age",
"directory": false,
"bytes": 7390311
},
{
"name": "state-hub-db-state_hub-20260910T023000Z.dump.age",
"directory": false,
"bytes": 7542258
},
{
"name": "state-hub-db-state_hub-20260911T023000Z.dump.age",
"directory": false,
"bytes": 7687798
},
{
"name": "state-hub-db-state_hub-20260912T023000Z.dump.age",
"directory": false,
"bytes": 7748393
},
{
"name": "state-hub-db-state_hub-20260913T023000Z.dump.age",
"directory": false,
"bytes": 7765176
},
{
"name": "state-hub-db-state_hub-20260914T023000Z.dump.age",
"directory": false,
"bytes": 7839953
},
{
"name": "state-hub-db-state_hub-20261005T023000Z.dump.age",
"directory": false,
"bytes": 9407746
},
{
"name": "state-hub-db-state_hub-20261006T023000Z.dump.age",
"directory": false,
"bytes": 9464763
},
{
"name": "state-hub-db-state_hub-20261007T023000Z.dump.age",
"directory": false,
"bytes": 9549640
},
{
"name": "state-hub-db-state_hub-20261008T023000Z.dump.age",
"directory": false,
"bytes": 9602097
},
{
"name": "state-hub-db-state_hub-20261009T023003Z.dump.age",
"directory": false,
"bytes": 9693941
},
{
"name": "state-hub-db-state_hub-20261010T023000Z.dump.age",
"directory": false,
"bytes": 9766236
},
{
"name": "state-hub-db-state_hub-20261011T023000Z.dump.age",
"directory": false,
"bytes": 9864346
},
{
"name": "state-hub-db-state_hub-20261012T023000Z.dump.age",
"directory": false,
"bytes": 9973489
},
{
"name": "state-hub-db-state_hub-20261013T023000Z.dump.age",
"directory": false,
"bytes": 10036580
},
{
"name": "state-hub-db-state_hub-20261014T023000Z.dump.age",
"directory": false,
"bytes": 10265536
},
{
"name": "state-hub-db-state_hub-20261015T023000Z.dump.age",
"directory": false,
"bytes": 10388491
},
{
"name": "state-hub-db-state_hub-weekly-20260906T023000Z.dump.age",
"directory": false,
"bytes": 6924847
},
{
"name": "state-hub-db-state_hub-weekly-20260913T023000Z.dump.age",
"directory": false,
"bytes": 7765176
},
{
"name": "state-hub-db-state_hub-weekly-20261011T023000Z.dump.age",
"directory": false,
"bytes": 9864346
}
],
"r01-user-engine-pg": []
},
"caller_auth_path": "auth/netkingdom/oidc/callback",
"caller_entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b",
"scope": "Backup/railiance-backups root and bounded identity/database subfolders; other accounts/folders unassessed",
"finished_at": "2026-10-15T21:52:28.155972+00:00"
}

View file

@ -0,0 +1,37 @@
"""Scoped S3 list-only fixtures; no credentials, live cluster or cloud."""
import importlib.util
from pathlib import Path
import pytest
ROOT=Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('s3_inventory',ROOT/'tools/bootstrap-recovery/inventory-scaleway.py')
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
class Client:
def __init__(self,pages):self.pages=iter(pages);self.calls=[]
def list_objects_v2(self,**kw):self.calls.append(kw);return next(self.pages)
def test_scoped_list_metadata_follows_continuation_without_reading_contents():
client=Client([{'IsTruncated':True,'NextContinuationToken':'next'},
{'Contents':[{'Key':'platform-pg/identity/backup.tar.age','Size':256}]}])
r=module.list_prefix(client,'platform-pg/identity/')
assert r['files']==[{'key':'platform-pg/identity/backup.tar.age','bytes':256}]
assert all(x['Bucket']==module.BUCKET and x['Prefix']=='platform-pg/identity/' for x in client.calls)
assert client.calls[1]['ContinuationToken']=='next'
def test_wal_and_base_presence_is_recorded_without_traversal():
client=Client([{'CommonPrefixes':[{'Prefix':'platform-pg/base/'},{'Prefix':'platform-pg/wals/'}]}])
r=module.inventory(client)
assert len(client.calls)==1 and len(r['platform-pg/']['directories'])==2
@pytest.mark.parametrize('key',['elsewhere/backup.age','platform-pg/../backup.age','platform-pg/private\nmarker.age'])
def test_foreign_or_unsafe_metadata_refuses(key):
with pytest.raises(module.Refused):module.list_prefix(Client([{'Contents':[{'Key':key,'Size':1}]}]),module.PREFIX)
def test_missing_pagination_state_refuses_incomplete_inventory():
with pytest.raises(module.Refused):module.list_prefix(Client([{'IsTruncated':True}]),module.PREFIX)

View file

@ -0,0 +1,43 @@
"""Read-only DAV metadata parser tests; no provider or credential access."""
import importlib.util
from pathlib import Path
import pytest
ROOT=Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('storage_inventory',ROOT/'tools/bootstrap-recovery/inventory-storage-share.py')
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
def body(path, kind='file', size='16', origin=''):
collection='<d:collection/>' if kind=='dir' else ''
return f'''<d:multistatus xmlns:d="DAV:"><d:response><d:href>{origin}{path}</d:href>
<d:propstat><d:status>HTTP/1.1 200 OK</d:status><d:prop><d:resourcetype>{collection}</d:resourcetype>
<d:getcontentlength>{size}</d:getcontentlength></d:prop></d:propstat></d:response></d:multistatus>'''.encode()
def test_only_closed_safe_metadata_is_returned():
raw=body(module.BASE+'bootstrap.tar.age')
assert module.parse_listing(raw,'')==[{'name':'bootstrap.tar.age','directory':False,'bytes':16}]
@pytest.mark.parametrize('path,origin',[(module.BASE+'%2e%2e/else.age',''),
('/other/account/backup.age',''),(module.BASE+'backup.age','https://unapproved.invalid')])
def test_foreign_and_escaping_references_refuse(path,origin):
with pytest.raises(module.Refused):module.parse_listing(body(path,origin=origin),'')
def test_non_numeric_size_and_oversized_response_refuse():
with pytest.raises(module.Refused):module.parse_listing(body(module.BASE+'backup.age',size='not-a-size'),'')
with pytest.raises(module.Refused):module.parse_listing(b'X'*(module.LIMIT+1),'')
def test_unrecognized_plaintext_files_are_not_exported():
assert module.parse_listing(body(module.BASE+'private-password.txt'),'')==[]
def test_unrelated_authenticated_role_cannot_read_retrieval_credential(monkeypatch):
calls=[]
monkeypatch.setattr(module,'bao',lambda args:calls.append(args) or object())
monkeypatch.setattr(module,'data',lambda result:{'data':{'policies':['default'],'path':'auth/netkingdom/oidc/callback'}})
with pytest.raises(module.Refused):module.inventory()
assert calls==[['token','lookup','-format=json']]

View file

@ -21,3 +21,17 @@ archive fixtures, never production credentials:
```bash
python3 -m pytest -q tests/test_bootstrap_recovery_transport.py
```
`inventory-storage-share.py` is a silent read-only child of the existing
attended Platform Backup-account procedure. It checks the caller's OIDC/admin
scope, reuses the owner custody accessor, and exports only bounded DAV names and
sizes. Warden revokes and cleans up the session. It never reads an age key or
object content and contains provider errors/redirects/proxies.
`inventory-scaleway.py` lists bounded metadata in the fixed backup bucket/prefix
using the existing scoped runtime delivery and pinned primary cluster. It uses
neither the bootstrap API key nor object GET/write/delete operations. Run through
the already admitted caller with cached boto3 1.40.18; it emits only its safe
receipt. Both inventories explicitly state their bounds and cannot prove asset
absence outside them, ciphertext integrity, decryption or native recovery.

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Read-only bounded S3 backup metadata through existing scoped runtime delivery.
No bootstrap API key, new grant, object content read, age key, provider write or
retention action. The caller must already have admitted cluster read authority.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
BUCKET='railiance-platform-pg-backup'
ENDPOINT='https://s3.nl-ams.scw.cloud'
PREFIX='platform-pg/'
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
class Refused(Exception):pass
def kube(args):
p=subprocess.run(['kubectl',*args,'-o','json'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=30)
if p.returncode:raise Refused()
return json.loads(p.stdout)
def list_prefix(client,prefix):
if not prefix.startswith(PREFIX) or not re.fullmatch(r'[A-Za-z0-9._/-]+',prefix) or '..' in prefix.split('/'):raise Refused()
directories=[];files=[];continuation=None
for _ in range(20):
args={'Bucket':BUCKET,'Prefix':prefix,'Delimiter':'/','MaxKeys':1000}
if continuation:args['ContinuationToken']=continuation
page=client.list_objects_v2(**args)
for row in page.get('CommonPrefixes',[]):
value=row['Prefix']
if not value.startswith(prefix) or not value.endswith('/') or not re.fullmatch(r'[A-Za-z0-9._/-]+',value) or '..' in value.split('/'):raise Refused()
directories.append(value)
for row in page.get('Contents',[]):
name=row['Key']
if not name.startswith(prefix) or not re.fullmatch(r'[A-Za-z0-9._/-]+',name) or '..' in name.split('/'):raise Refused()
if name.endswith(('.age','.json','.yaml')):
size=row['Size']
if type(size) is not int or size<0:raise Refused()
files.append({'key':name,'bytes':size})
if not page.get('IsTruncated'):break
continuation=page.get('NextContinuationToken')
if not continuation:raise Refused()
else:raise Refused()
return {'directories':sorted(set(directories)),'files':sorted(files,key=lambda x:x['key'])}
def inventory(client):
pending=[(PREFIX,0)];result={}
while pending:
prefix,depth=pending.pop(0)
if prefix in result:raise Refused()
if len(result)>=64:raise Refused()
row=list_prefix(client,prefix);result[prefix]=row
if depth<3:
for child in row['directories']:
# Native database WAL/base trees are not application/key-file
# archives. Record their presence without enumerating every WAL.
if any(x in child.split('/') for x in ['wals','base']):continue
pending.append((child,depth+1))
return result
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True,
'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False,
'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'}
try:
if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused()
delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3'])
if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused()
values=delivered['data']
# Same governed runtime delivery used by Platform's archive verifier;
# explicit credentials avoid SDK ambient/profile/metadata discovery.
import boto3
from botocore.config import Config
client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams',
aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(),
aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(),
config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30,
retries={'max_attempts':1},s3={'addressing_style':'path'}))
r['prefixes']=inventory(client);r['status']='metadata_inventory_complete'
r['runtime_delivery_uid']=delivered['metadata']['uid']
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
return int(r['status']=='failed')
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Silent read-only inventory in the existing Platform Backup-account procedure.
Runs only inside the attended platform-admin envelope. Reuses Platform's owner
custody accessor and fixed provider/account contract; no age key is read and no
backup is downloaded, uploaded, deleted, rotated or granted.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import sys
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
ROOT=Path(__file__).resolve().parents[2]
OWNER=ROOT.parent/'railiance-platform/scripts'
sys.path.insert(0,str(OWNER))
from state_hub_preflight_lane import bao, data
from migrate_nextcloud_backup_account import HOST, OPERATOR_PATH
BASE='/remote.php/dav/files/Backup/railiance-backups/'
LIMIT=4*1024*1024
NAME=re.compile(r'[A-Za-z0-9._-]{1,160}\Z')
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def parse_listing(raw, relative):
if len(raw)>LIMIT:raise Refused()
rows=[]
for item in ET.fromstring(raw).findall('{DAV:}response'):
href=urllib.parse.urlsplit(item.findtext('{DAV:}href') or '')
if href.netloc and href.netloc!=urllib.parse.urlsplit(HOST).netloc:raise Refused()
path=urllib.parse.unquote(href.path)
if not path.startswith(BASE+relative):raise Refused()
name=path[len(BASE+relative):].rstrip('/')
if not name:continue
if not NAME.fullmatch(name) or name in {'.','..'}:raise Refused()
for row in item.findall('{DAV:}propstat'):
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
prop=row.find('{DAV:}prop')
if prop is None:continue
folder=prop.find('{DAV:}resourcetype/{DAV:}collection') is not None
size=prop.findtext('{DAV:}getcontentlength')
if size is not None and not size.isdigit():raise Refused()
# Export only known-safe operational names/size, not arbitrary DAV
# properties, URLs, account metadata or raw provider responses.
if folder or name.endswith(('.age','.json','.yaml')):
rows.append({'name':name,'directory':folder,'bytes':int(size) if size else None})
if len(rows)>10000 or len({r['name'] for r in rows})!=len(rows):raise Refused()
return sorted(rows,key=lambda x:x['name'])
def listing(relative,credentials):
if relative and (not relative.endswith('/') or not NAME.fullmatch(relative[:-1])):raise Refused()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
req=urllib.request.Request(HOST+BASE+relative,
data=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/></d:prop></d:propfind>',
method='PROPFIND',headers={'Authorization':'Basic '+auth,'Depth':'1','Content-Type':'application/xml'})
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
with opener.open(req,timeout=30) as response:
if response.status!=207 or response.url!=HOST+BASE+relative:raise Refused()
raw=response.read(LIMIT+1)
return parse_listing(raw,relative)
def inventory():
# Lookup payload contains a token: retain only safe caller/role facts.
identity=data(bao(['token','lookup','-format=json']))['data']
if 'platform-admin' not in identity.get('policies',[]) or 'root' in identity.get('policies',[]):raise Refused()
if not str(identity.get('path','')).startswith('auth/netkingdom/'):raise Refused()
credentials=data(bao(['read','-format=json',OPERATOR_PATH]))['data']['data']
if credentials.get('BACKUP_USERNAME')!='Backup' or not isinstance(credentials.get('BACKUP_PASSWORD'),str) or not credentials['BACKUP_PASSWORD']:raise Refused()
root=listing('',credentials)
folders={}
for row in root:
if row['directory'] and re.match(r'^(?:r01-|core-|identity|sso|bootstrap|net-kingdom|openbao)',row['name']):
if len(folders)>=32:raise Refused()
folders[row['name']]=listing(row['name']+'/',credentials)
return {'root':root,'folders':folders,'caller_auth_path':identity['path'],
'caller_entity_id':identity.get('entity_id'),'scope':'Backup/railiance-backups root and bounded identity/database subfolders; other accounts/folders unassessed'}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.storage-share-inventory.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
'read_only':True,'age_keys_read':False,'backup_contents_downloaded':False,'secret_values_exported':False,
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'owner_sources':[{'path':str(OWNER/name),'sha256':hashlib.sha256((OWNER/name).read_bytes()).hexdigest()} for name in ['state_hub_preflight_lane.py','migrate_nextcloud_backup_account.py']]}
try:r.update(inventory());r['status']='metadata_inventory_complete'
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
return int(r['status']=='failed')
if __name__=='__main__':raise SystemExit(main())

View file

@ -438,6 +438,27 @@ reviewed isolated target/procedure and actual owner/native acceptance. Evidence:
`docs/evidence/2026-10-15-bootstrap-recovery/`; full verification results are
recorded there. No workplan completion is asserted.
2026-10-15 off-host discovery follow-through: operator reports holding the
`railiance-backup` age key independently; no private key was received/read.
The read-only Storage Share inventory completed through the existing attended
Platform Backup-account custody procedure; Warden exited 0, confirming session
self-revocation/cleanup. The inspected account has a current privacyIDEA database
ciphertext (`r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age`,
89,129,309 bytes) and the October14 OpenBao snapshot. The inspected folders do
not contain a bootstrap/identity-file bundle. Bounded Scaleway metadata inventory
under the existing scoped backup delivery also completed; it found database
base/WAL trees and an application archive subtree, with no identity-file bundle
in the inspected scope. No bootstrap API key, age key or object content was read
by these inventories; other accounts/buckets/prefixes remain unassessed.
Thirteen fixture checks pass for contained caller scope, metadata/path bounds,
pagination and malformed/foreign responses. Requested the human's hidden-terminal
key check against the backup owner's declared public recipient. Matching that
recipient remains distinct from actual decryption and full recovery; the
bootstrap `keys/age.pub` is not assumed to be the same recipient. T02/T03 remain
open. Evidence: `offsite-discovery-summary.json` and the two exact inventory
receipts in the existing October15 evidence folder.
## Verify owner-approved recovery and current native callers
```task
@ -445,7 +466,7 @@ id: NK-WP-0049-T03
status: wait
priority: high
depends_on: [NK-WP-0049-T02]
blocking_reason: "Exact off-host artifact/coverage and independently held matching key remain unconfirmed after Scaleway/Storage Share destination identification. Requires completed T02 owner/runtime and legacy-credential disposition, then the exact reviewed isolated recovery ceremony and native consumer acceptance. No live bootstrap/root retrieval or deferred exposure rotation is authorized by source tests."
blocking_reason: "A recent off-host PI database ciphertext is identified, but matching full identity data/key/config coverage and actual independent-key decryption remain unverified after bounded Storage Share/Scaleway inventory. Requires completed T02 owner/runtime and legacy-credential disposition, then the exact reviewed isolated recovery ceremony and native consumer acceptance. No live bootstrap/root retrieval or deferred exposure rotation is authorized by source tests."
state_hub_task_id: "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac"
```