Make S1 handoff read-only by default
All checks were successful
CI Smoke / source-contract (push) Successful in 8s
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 12:41:23 +02:00
parent 24b799ec59
commit 40e295e3bd
16 changed files with 637 additions and 46 deletions

View file

@ -179,9 +179,12 @@ interpreting an expected-red exception.
`make s1-handoff` validate source contracts, require a clean revision, run each
host separately, fail the aggregate on any non-zero result, require TAP hashes
for a pass, and record a freshness boundary. Dry-run output is forcibly
`not-run`. The attended all-host run waits for an environment with Ansible and
reviewed permission to refresh the on-host Goss surface; this workstation has
no `ansible-playbook`. No host was contacted.
`not-run`. The live playbook is now remotely read-only: a static contract
rejects mutating modules and arbitrary commands, it requires the installed
Goss surface to match the locally rendered profile digest, and it writes only
controller-side evidence. Refresh is a separate exact-approval interface. The
attended all-host run now waits only for Ansible plus short-lived SSH access;
no host was contacted.
## T06 — Repair and enforce the secret-source contract
@ -286,10 +289,14 @@ decryption receipt or recipient change was attempted.
Current evidence (2026-08-23):
- Python unit suite: 25 tests pass.
- Python unit suite: 30 tests pass, including rejection of a remote template,
arbitrary remote command, non-delegated controller write, and unreviewed
Goss command surface in the handoff path.
- Terraform 1.9.8 mock-provider tests: 2 pass.
- Ansible 2.17.13 syntax checks: bootstrap, verify, and firewall pass in a
disposable environment.
- Ansible 2.17.13 syntax checks: bootstrap, read-only verify, explicit
verify-refresh, and firewall pass in a disposable environment. A disposable
render comparison also proves the template lookup digest matches the bytes
produced by Ansible's deployment template action.
- Both profile-specific Goss templates render and parse as YAML.
- Inventory, baseline, secret metadata, receipt, shell syntax, Python compile,
and whitespace checks pass.

View file

@ -0,0 +1,178 @@
---
id: RAIL-HO-WP-0012
type: workplan
title: "Close the encrypted S1 backup and recovery loop"
domain: financials
repo: railiance-infra
status: proposed
owner: codex
topic_slug: railiance
created: "2026-08-23"
updated: "2026-08-23"
related:
- RAIL-HO-WP-0011
---
# RAIL-HO-WP-0012 — encrypted S1 backup and recovery loop
## Goal
Turn the existing local `railiance-backup-s1` helper into a fail-closed,
scheduled, off-host, and restore-tested S1 recovery capability. Preserve the
repository boundary: this covers host operating-system configuration and
package-selection evidence only, not Kubernetes, database, Forgejo, or
application data.
This workplan does **not** authorize deployment of a systemd timer, access to an
age private key, an off-host upload, a live `/etc` restore, or deletion of a
retained artifact. Each live action retains its normal owner review and exact
operator approval.
## Current State and Risks
`tools/cmd/railiance-backup-s1` currently creates age-encrypted OS-configuration
and package-selection files under `/opt/backup/railiance/infra` and retains the
latest seven of each. It is not scheduled, has no off-host transfer, produces
no manifest or receipt, has no restoration tool or drill, duplicates an age
recipient in executable source, suppresses individual copy failures, and can
leave ambiguous partial output. The output is therefore a useful local helper,
not current proof of recoverability.
## Delivery Order
T01 → T02 → T03 establish a locally testable contract. T04 and T05 may proceed
after T02 but need separate deployment/custody review. T06 depends on all prior
tasks and an attended isolated drill.
## T01 — Define the S1 backup declaration and safety contract
```task
id: RAIL-HO-WP-0012-T01
status: todo
priority: high
```
Create a machine-readable declaration for included paths, optional paths,
artifact classes, public encryption recipients, retention, and excluded
higher-layer data. Validate that every selected path is absolute and bounded,
that no private-key or credential path can be selected, and that the recipient
contract cannot drift silently from the repository's approved public metadata.
**Done when:** the current intended S1 files resolve deterministically, unsafe
or relative paths fail closed, optional absence is explicit, and validation
requires neither root nor a decryption key.
## T02 — Produce atomic encrypted artifacts, manifests, and receipts
```task
id: RAIL-HO-WP-0012-T02
status: todo
priority: high
```
Refactor the backup helper around the T01 declaration. Check privileges before
creating the destination, stage output on the same filesystem, fail on an
unreadable required input, encrypt before publication, and atomically publish
only complete artifacts. Emit a metadata-only manifest and receipt containing
host identity, source revision, declaration digest, artifact class, encrypted
size/hash, completion time, and retention decision.
Exercise it against disposable fixture roots and output directories. Tests
must prove a failed collection or encryption cannot produce a passing receipt
or a final-named partial artifact.
**Done when:** a fixture backup is deterministic in membership, encrypted
before publication, fully described by redacted metadata, and failure-injection
tests leave no ambiguous success state.
## T03 — Add non-destructive inspection and isolated restore validation
```task
id: RAIL-HO-WP-0012-T03
status: todo
priority: high
```
Provide a restore command that defaults to inspection or extraction beneath an
explicit empty staging directory. It must verify receipt/manifest hashes before
decryption, suppress decrypted content from logs, reject path traversal and
links escaping the staging root, and compare restored membership and critical
file modes with the declaration.
Restoring over live `/etc` is outside this task and must remain impossible
without a separately designed exact-approval interface.
**Done when:** a fixture artifact restores into disposable storage, tampering
and unsafe members fail closed, no plaintext enters repository paths or logs,
and no default invocation can overwrite a host file.
## T04 — Define scheduling, freshness, and local retention
```task
id: RAIL-HO-WP-0012-T04
status: todo
priority: medium
```
Add source-controlled systemd service/timer units and Ansible deployment for a
bounded backup cadence. Define maximum acceptable age, failure state, disk
budget, atomic pruning, and an operator status interface. Keep timer deployment
separate from source validation and require explicit host-change approval.
**Done when:** unit rendering and calendar behavior test locally, stale/missing
backup status fails, pruning never crosses the declared backup directory, and
no timer is installed merely by running a verification command.
## T05 — Integrate the governed write-only off-host lane
```task
id: RAIL-HO-WP-0012-T05
status: wait
priority: high
```
Coordinate with `railiance-platform` through the existing
`railiance-backup-offsite-lane` route. Upload only completed encrypted
artifacts plus metadata manifests, use a host-local or workload-owned
credential projection, and record a non-secret remote object identity and
encrypted hash. Do not place WebDAV credentials or an age private key in this
repository, State Hub, command output, or agent prompts.
**Done when:** the owner accepts the exact projection and write contract, a
dry-run proves object naming and collision behavior, a controlled upload is
visible through owner-provided metadata, and local/off-host retention cannot
delete the only recoverable copy.
## T06 — Perform an attended isolated restore drill
```task
id: RAIL-HO-WP-0012-T06
status: wait
priority: high
```
Select a fresh off-host artifact by metadata, retrieve it through the governed
lane, and restore it into an isolated disposable filesystem. Prove manifest
integrity, expected membership and modes, usable SSH/fail2ban/UFW configuration
syntax, package-selection readability, and cleanup of all decrypted temporary
material. Do not restore over a live host in this drill.
**Done when:** a metadata-only drill receipt records artifact and source
identity, checks, duration, cleanup, and result; an independent observer can
distinguish a successful recovery from a backup-only claim without seeing
decrypted configuration.
## Acceptance
- [ ] S1 backup membership, exclusions, recipients, and retention are declared
and validated from source.
- [ ] Backup publication is atomic, encrypted, and accompanied by a safe
manifest and receipt.
- [ ] Restore defaults to isolated inspection and rejects tampering, traversal,
and accidental live overwrite.
- [ ] Scheduling and freshness checks are source-controlled but deployed only
through an explicit host-change approval.
- [ ] A governed off-host copy and an attended isolated restore drill prove the
selected S1 state is recoverable.
- [ ] No cluster, platform, tenant, secret-issuance, or private-key custody
responsibility is absorbed into this repository.