Implement governed S1 backup recovery loop
Some checks failed
CI Smoke / source-contract (push) Failing after 9s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
codex 2026-08-23 13:13:13 +02:00
parent 40e295e3bd
commit 295bf43d54
16 changed files with 1623 additions and 95 deletions

View file

@ -0,0 +1,95 @@
# 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`](../spec/s1-backup.yaml).
## Safe source gate
Run this without root, host access, or an age private key:
```bash
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:
```bash
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:
```bash
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.
## Inspection and isolated restore
Inspection verifies receipt, manifest, declaration, encrypted hashes, sizes,
and membership without a private key:
```bash
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:
```bash
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:
```bash
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 transfer remains `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 action is implemented or implied here.