railiance-infra/docs/s1-backup-recovery.md
codex d0a5a27926
All checks were successful
CI Smoke / source-contract (push) Successful in 11s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Prepare governed S1 offsite upload
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
2026-08-23 14:11:00 +02:00

5.2 KiB

S1 encrypted backup and isolated recovery

This repository can collect only the declared S1 operating-system configuration and Debian package-selection evidence. It does not back up Kubernetes state, databases, Forgejo, application data, user homes, secrets, or private keys. The machine-readable boundary is spec/s1-backup.yaml.

Safe source gate

Run this without root, host access, or an age private key:

make validate-s1-backup

The declaration must agree exactly with the public age recipients in .sops.yaml. Fixture tests prove encrypted atomic publication, failure cleanup, receipt integrity, isolated restore, traversal rejection, freshness, and exact retention approval. This command never installs or starts a timer.

Backup and status

On a host, root can create one bundle:

sudo tools/cmd/railiance-backup-s1
make s1-backup-status

A final s1-backup-YYYYMMDDTHHMMSSZ/ directory appears only after both artifacts are encrypted and the manifest and receipt are durable. The receipt contains hashes, sizes, host identity, source revision, declaration digest, and the retention decision, but no decrypted content or secret material. Status fails for missing, stale, over-budget, over-retained, or tampered newest evidence.

Retention is deliberately not an unattended delete. Render the current exact candidate set, review it, and pass back the one-use approval derived from the candidate receipt hashes:

make s1-backup-prune-plan
make s1-backup-prune APPROVE_S1_BACKUP_PRUNE=PRUNE-S1-BACKUPS-...

If the set changes between those commands, pruning fails closed. Candidates are atomically moved beneath a quarantine directory inside the declared backup root before removal; paths outside that root are never accepted. Production pruning also refuses every candidate that lacks a passing off-site receipt bound to the current local manifest.

Governed off-site owner interface

The upload contract is source-controlled in spec/s1-offsite.yaml. It packages the two encrypted artifacts and their metadata into one deterministic tar envelope, avoiding a remotely visible partial multi-object backup. Upload uses an immutable object name and If-None-Match: *; a collision fails without a success receipt. Neither the endpoint, response body, nor credential is placed in the receipt.

The contract is intentionally pending railiance-platform acceptance. Render the exact review phrase without credentials:

make s1-offsite-review

After the owner records acceptance of that exact digest, a fresh bundle can be planned without access to the lane:

make s1-offsite-plan BUNDLE=/opt/backup/railiance/infra/s1-backup-...

Only the exact resulting envelope approval can enter the contained upload path. warden access injects the primary token into the child environment; it is never accepted in argv, files, State Hub, or command output:

make s1-offsite-upload \
  BUNDLE=/opt/backup/railiance/infra/s1-backup-... \
  APPROVE_S1_OFFSITE_UPLOAD=UPLOAD-S1-OFFSITE-...

This command remains disabled while spec/s1-offsite-owner-acceptance.yaml is pending. Owner acceptance must also confirm that the secret-shaped default discovered in the owner repository is retired or rotated; its value must never be copied into this repository or an approval message. The current route still lacks an authoritative workload identity, so the reviewed interface is attended and is not attached to the systemd timer.

Inspection and isolated restore

Inspection verifies receipt, manifest, declaration, encrypted hashes, sizes, and membership without a private key:

make s1-restore-inspect BUNDLE=/opt/backup/railiance/infra/s1-backup-...

An attended operator may then extract into an explicit absent or empty staging directory outside this repository:

make s1-restore-isolated \
  BUNDLE=/opt/backup/railiance/infra/s1-backup-... \
  DEST=/tmp/s1-restore-drill \
  IDENTITY=/operator-controlled/path/identity.age

The command suppresses decryption output, validates every archive member and mode before extraction, rejects escaping links and traversal, and writes a metadata-only restore receipt. It refuses /, /etc, /home, /root, /var, repository paths, and non-empty destinations. There is no live restore mode.

Timer deployment approval interface

The timer runs daily at 02:15 UTC with up to 30 minutes of jitter. Its service can write only the declared backup root under systemd's filesystem controls. Deployment is a host mutation and is separate from bootstrap and verification:

make s1-backup-deploy \
  HOST=Railiance01 \
  APPROVE_S1_BACKUP_DEPLOY=DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER

The Make target also refuses a dirty source tree. The playbook independently checks the approval and pins the deployed wrapper to the reviewed 40-character source revision. This approval installs and enables the timer only; it does not authorize pruning, off-host upload, private-key access, or a restore.

Off-host execution remains pending under RAIL-HO-WP-0012-T05 and belongs to the governed railiance-backup-offsite-lane. An attended off-host isolated drill remains RAIL-HO-WP-0012-T06; neither live action is implied here.