# Isolated original-OpenBao restore (RPF-WP-0064) Adapter: `scripts/openbao-isolated-restore/`. Workplan: `workplans/RPF-WP-0064-isolated-original-openbao-restore.md`. This restores the original OpenBao Raft snapshot into a disposable, networkless container on the founder workstation. It unseals the snapshot with the original Shamir shares and reads back configuration metadata only. It claims no restore capability for production, no consumer continuity, no live JWT login, no phase B network and no owner acceptance. Every receipt lists these under `claims_not_made`. ## Fixed properties | Property | Value | | --- | --- | | Where | Founder workstation Docker only. The launcher refuses `railiance01*` hosts and unsets `KUBECONFIG`, `BAO_*` and `VAULT_*`. | | Image | `quay.io/openbao/openbao@sha256:`, run with `--pull=never`. The admission must name an exact digest. Original-material runs also require `PINNED_IMAGE_DIGEST` in `restore_adapter.py`, which T06 sets after rapp-openbao confirms the production pin. Until then they refuse. | | Container | `--network none`, `--read-only`, `--cap-drop ALL`, `no-new-privileges`, `--user 100:1000`, and tmpfs only for `/openbao/data`, `/openbao/config`, `/openbao/file`, `/openbao/logs` and `/tmp`. The image's VOLUME paths are overlaid with tmpfs, so no Docker volume is created. There are no binds, published ports or kubeconfig. | | Listener | `127.0.0.1:8200` inside the container namespace only. Unauthenticated generate-root is enabled on this listener alone, because OpenBao 2.x disables it by default and the readback credential needs it. | | Network proof | `docker container inspect`: `NetworkMode=none`, networks `["none"]`, no ports, mounts or binds, the exact tmpfs set and not privileged. `/proc/net/dev` inside the container lists only `lo`. Two `wget` probes (`192.0.2.1` and `kc.coulomb.social`) must both fail. | | Deadline | 45 minutes (`max_duration_seconds: 2700`) from the session start, capped by the admission `not_after`. A reaper process enforces it as a backstop, with 60 s of grace. | | Admission namespace | `railiance-openbao-restore-attempt`. It is an SSH signature over the exact admission bytes. The anchor is `/etc/railiance-openbao-restore/allowed_signers`, overridable with `RAILIANCE_OPENBAO_RESTORE_ANCHOR`. For original material the anchor must not be writable by the running account. | ## Custody path The prompts are hidden (`getpass` on the controlling TTY). Custody values never pass through argv, the environment, files or logs. 1. **Backup age identity.** It is checked against the admission's `age_recipient` with `age-keygen -y`, then held in a sealed memfd. It is passed to `age -d` only as `/dev/fd/N` and closed straight after decryption. 2. **Snapshot.** The ciphertext size and sha256 must match the admission. The plaintext sha256 must match the RPF-WP-0015 receipt's `snapshot_sha256` (admission `plaintext_sha256`). The plaintext is held in a sealed memfd and streamed to `bao operator raft snapshot restore -force /dev/stdin` over `docker exec` stdin. It never reaches disk. 3. **Throwaway initialization.** A 1-of-1 init of the empty target. Its share and credential stay in process memory. The restore invalidates them, and a failed `lookup-self` after the restore proves this. 4. **Two original unseal shares.** After the restore the target must report `sealed: true`. The two shares then unseal it, and the restored `cluster_id` and Raft applied index are compared with the admission. 5. **Readback credential.** Decision for T02 review: generate-root from the two shares already entered, with the OTP decoded in memory. The operator OIDC path cannot work without a network. The credential is revoked with `revoke-self` before teardown, and the revocation is verified. ## Readback Readback is metadata only. The allow-list in `ALLOWED_READS` contains no `/data/` path. | Object | Path | Compared with | | --- | --- | --- | | `auth_mount_keycape_services` | `sys/auth` | type `jwt` | | `auth_mount_netkingdom` | `sys/auth` | type `oidc`/`jwt` | | `kv_mount_platform` | `sys/mounts` | type `kv`, version 2 | | `keycape_services_config` | `auth/keycape-services/config` | `openbao/auth/keycape-services-config.json` | | `secrets_engine_jwt_role` | `auth/keycape-services/role/secrets-engine-openbao` | `openbao/auth/secrets-engine-jwt-role.json` | | `netkingdom_oidc_config` | `auth/netkingdom/config` | `openbao/auth/netkingdom-oidc-config.json` (except `_note`) | | `platform_admin_role` | `auth/netkingdom/role/platform-admin` | `openbao/auth/netkingdom-platform-admin-role.json` | | `policy_secrets_engine_login_self` | `sys/policies/acl/secrets-engine-login-self` | `openbao/policies/secrets-engine-login-self.hcl` | | `kv_registration_metadata` | `platform/metadata/workloads/keycape/secrets-engine-openbao` | current version present, not deleted, not destroyed | The comparison projects only the fields the source file declares. Durations such as `5m` are normalised to seconds, and string lists are sorted. The receipt records each object as `present`, `source_match` and a typed `observed_sha256` of the projected fields, never the values. A mismatch means the 2026-10-14 snapshot differs from the current source. That is a finding, not a repair. ## Outcomes - `restored`: every required object is present and matches its source. - `refused`: an object is missing (`object_missing`) or differs (`object_mismatch`). Nothing is repaired. - `aborted`: any other failed check, a timeout, Ctrl-C/SIGTERM/SIGHUP (`operator_abort`) or the loss of the parent process (`parent_lost`, written by the reaper). Teardown always runs: revoke the credential, close the memfds, wipe the share buffers, `docker rm -f -v`, then a `docker container inspect` that must fail. The receipt is written with mode 0600 and `O_EXCL`, then checked by the independent verifier. ## Receipt v2 and verifier The schema is `railiance-openbao-restore-receipt/v2`. It extends `docs/openbao-restore-drill-evidence.example.json`; the shape is in `docs/openbao-restore-receipt-v2.example.json`. All digests are typed `sha256:`, and no key name pairs with a bare hex value. The verifier `scripts/openbao-isolated-restore/verify-restore-receipt.py` (`make openbao-verify-restore-receipt`) does not import the adapter. It rejects unknown or missing fields, duplicate keys and placeholders. It also rejects credential, share, age, PEM, hex or base64 secret shapes, `data` fields and KV `/data/` paths. It requires teardown evidence and the exact `claims_not_made` set, and checks result consistency, the 45-minute deadline and `--not-after`. Finally it compares `source_sha256` with a clean checkout at `adapter_commit`. The receipt stays outside Git and is referenced by its sha256. ## Admission (what T05 signs) The shape is in `docs/openbao-restore-admission.example.json`, schema `railiance-openbao-restore-admission/v1`: - `attempt_id`: `RPF-WP-0064-T07-` - `phase: "A"`, `phase_b_admitted: false`, `material: "original"` - `not_before`, `not_after` (at most 24 h), `max_duration_seconds: 2700`, `abort_operator` - `image`: the T06-confirmed digest reference - `adapter_commit` and `source_sha256` for all ten closure files: four adapter files, four auth JSON files, the login-self policy and the JWT-login contract - `snapshot`: `ciphertext_sha256`, `ciphertext_bytes`, `plaintext_sha256`, `age_recipient`, `source_cluster_id`, `source_applied_index`, `source_receipt_sha256` (taken from the RPF-WP-0015-T03 receipt and the stored object) - `unseal: {key_threshold: 2, key_shares: 3}`, `readback_credential: "generate-root-from-entered-shares"` Sign it: ```bash ssh-keygen -Y sign -f ~/.ssh/ -n railiance-openbao-restore-attempt admission.json ``` ## Attended run (T07) ```bash scripts/openbao-isolated-restore/run-isolated-restore.sh \ /abs/admission.json /abs/admission.json.sig sha256: \ /abs/.age /abs/receipt-dir make openbao-verify-restore-receipt RECEIPT=/abs/receipt-dir/.json SOURCE_ROOT= ``` `preflight.py` runs first and is metadata only. It checks the admission bytes and signature, the window, the source closure at a clean `adapter_commit`, the pinned image cached locally, the ciphertext hash and age header, `/dev/shm`, the required tools and that no stale `rpf64-restore-*` container exists. ## Phase B (design stub, not implemented) Phase B is a separate admission, not part of this adapter. It would attach a single `docker network create --internal` network holding only this restored target and the isolated NK49 KeyCape. Inside it `kc.coulomb.social` resolves to the isolated KeyCape, using its own trusted TLS chain. Egress is shown to be refused before admission. Custody stays the same. The secrets-engine consumer run (SECRETS-WP-0015-T04) owns the login attempt. ## Rehearsal (T04) ```bash make openbao-isolated-restore-test # offline: admission, verifier adversarial fixtures, launcher make openbao-isolated-restore-image # pull the public image (no custody) make openbao-isolated-restore-rehearsal # native: real OpenBao, --network none, generated material ``` The workplan T04 note records what ran natively and what was mocked.