Codify railiance-backup practices — 3-2-1 model, encryption, retention, Forgejo Option A (2026-07-09), credential separation, and monitoring. Mark T02 done.
13 KiB
Backup Policy
Status: adopted (initial)
Updated: 2026-07-07
Owner: Bernd
Scope: Coulomb Social / Helix Forge — Railiance backup lanes
Register: AssetRegister.yaml (stream inventory) · DataRegister.yaml (data classes)
This policy translates live railiance-backup practices into enforceable rules
with verification steps. It governs what must be backed up and how; backup
scripts run in owner repos (railiance-cluster, railiance-infra,
railiance-platform, railiance-apps).
1. Operating principle
Every T1 data class must have a named backup stream, encryption, retention, offsite copy (where required), monitoring, and restore evidence.
Servers are disposable; backups are not. Rebuild + restore is the default recovery pattern — not server resurrection.
Honest status reporting is required: streams marked missing, not_wired, or
at_risk in AssetRegister.yaml are policy violations until closed.
2. Architecture — 3-2-1 + immutable/offline
Target model (per specs/ResilienceControlSetup.md §4):
| Copy | Role | Implementation today |
|---|---|---|
| Production | Live data | Production hosts, PVCs, databases |
| Local fast rollback | Same-site recovery | /opt/backup/railiance/{infra,cluster}/; ~/.cache/railiance/backups/ |
| Offsite encrypted | Survives host loss | Nextcloud WebDAV file drop (upload-only) |
| Immutable/offline | Survives hostile delete | Phase 1: upload-only drop token (no delete from production); weekly offline copy — not yet implemented |
3-2-1 compliance by stream:
| Stream | 3 copies | 2 media | 1 offsite | Notes |
|---|---|---|---|---|
| Dev workstation | ✓ (prod + local cache + Nextcloud) | ✓ (disk + cloud) | ✓ | Operational |
| S1 OS config | ✗ (prod + local only) | ✗ (disk only) | ✗ | Gap — needs offsite upload |
| S2 k8s runtime | ✗ (prod + local only) | ✗ (disk only) | ✗ | Gap — needs offsite upload |
| S3 platform | — | — | — | Tool missing |
| Forgejo production | ✗ (prod + drill /tmp only) | partial | intended | Automation not wired |
| Gitea canonical | ✗ | ✗ | ✗ | Manual pre-cutover only |
3. Encryption standard
All backup artifacts must be encrypted with age before leaving the host or entering offsite storage.
Key custody
| Key | Operational location | Recovery copy | Used by |
|---|---|---|---|
| Dev-lane age key | ~/.config/age/railiance-backup.key |
Password manager | railiance-backup (dev workstation) |
| S1/S2 age key | ~/.config/sops/age/keys.txt |
Password manager + SOPS recovery | railiance-backup-s1, railiance-backup-s2 |
| Platform/Forgejo lane | Platform backup public key (TBD in OpenBao) | Password manager | Intended S3 + Forgejo dumps |
Rules:
- Operational keys:
chmod 700on~/.config/age;chmod 600on key files. - Password-manager recovery copy is mandatory — it is the only key that survives hardware failure.
- Never store private keys in Git, workplans, State Hub, or chat.
- Offsite credentials live in OpenBao (
platform/workloads/railiance/backup/offsite-lane); not on production hosts.
Verification
| Check | Frequency | Pass condition | Owner |
|---|---|---|---|
| Recovery key in password manager | Monthly | Operator confirms copy exists and decrypts a test artifact | Bernd |
| Operational key permissions | Monthly | stat shows 600 on key file, 700 on parent dir |
Bernd |
| Cold decrypt drill | Quarterly | Decrypt one artifact from each active lane without using production host | Bernd |
4. Destination policy
Off-node — Nextcloud WebDAV file drop
Used by: dev workstation lane (operational); Forgejo + platform lane (adopted, not fully wired).
- Endpoint: filesdrop URL —
https://<host>/public.php/dav/filesdrop/<token>/<filename> - Not valid:
/public.php/webdav/(returns HTTP 409 on current Nextcloud) - Upload method:
curl -Twith-u <token>:— upload-only; no rclone listing - Properties: upload-only token cannot read or delete existing artifacts from the production host — supports credential separation (R3)
- Human link:
260225-backup-dropoff-link.txt(gitignored); credentials in OpenBao
On-node — local encrypted cache
| Path | Lane | Retention |
|---|---|---|
~/.cache/railiance/backups/ |
Dev workstation | 7 per type |
/opt/backup/railiance/infra/ |
S1 OS config | 7 per type |
/opt/backup/railiance/cluster/ |
S2 k8s runtime | 7 per type |
Local caches are fast rollback only — not a substitute for offsite copy.
5. Retention policy
| Lane | Daily rotations | Weekly rotations | Local prune | Status |
|---|---|---|---|---|
| Dev workstation | N/A (continuous daily) | N/A | 7 per type | operational |
| S1 / S2 | N/A (manual) | N/A | 7 per type | operational |
| Forgejo + platform (adopted 2026-07-09) | 14 | 4 (Sunday promote or explicit job) | 7 per type | not wired |
| Gitea pre-cutover | Point-in-time of last manual dump | — | — | at risk |
Prune logic: find … \| sort -r \| tail -n +8 \| xargs rm (keep last 7) in
railiance-backup, railiance-backup-s1, railiance-backup-s2.
6. Per-stream mechanisms
Canonical inventory: AssetRegister.yaml. Summary:
| Register ID | Tool | Schedule | Destination | Status |
|---|---|---|---|---|
backup-dev-workstation |
railiance-backup |
Daily 02:00 cron | Nextcloud + local cache | operational |
backup-s1-os-config |
railiance-backup-s1 |
Manual make backup |
/opt/backup/railiance/infra/ |
operational |
backup-s2-k8s-runtime |
railiance-backup-s2 |
Manual make backup |
/opt/backup/railiance/cluster/ |
operational |
backup-s3-platform |
railiance-backup |
TBD | Nextcloud (intended) | missing |
backup-forgejo-production |
forgejo dump + pg_dump |
Daily (intended) | Nextcloud (adopted) | not_wired |
backup-gitea-canonical |
gitea dump / mirror |
Pre-cutover manual | None automated | at_risk |
Related streams (tracked, not yet full policy coverage):
backup-adjacent-sso— daily CronJobs (privacyidea-backup,authelia-backup,lldap-backup) on railiance01backup-apps-pg— CNPGapps-pg; no ScheduledBackup CR
Not in git backups: Git repos are protected by push-to-Gitea remotes. Preflight verifies clean and pushed state before infrastructure work.
7. Adopted standard — Forgejo backup (Option A, 2026-07-09)
Decision: Extend the existing Railiance platform backup lane — age-encrypted artifacts uploaded to Nextcloud WebDAV.
| Component | Method | Schedule |
|---|---|---|
| Forgejo blob state | forgejo dump zip (repos, OCI/npm/generic packages, attachments, LFS, avatars) |
Daily |
| PostgreSQL | pg_dump from CNPG forgejo-db (logical; no WAL/PITR in Phase 1) |
Daily |
| Encryption | age (platform backup public key) | Per artifact |
| Destination | Nextcloud WebDAV file drop | Upload after each run |
| Restore proof | forgejo-restore-drill.sh from automated backup |
Quarterly |
RPO / RTO: 24h / 4h.
Implementation owners:
railiance-platform— cron, age encrypt, Nextcloud upload, retention prunerailiance-infra— restore runbook + quarterly drill evidencerailiance-apps— no backup secrets in Git; dump runs against live pod
Phase 1 exclusions: CNPG WAL archiving to S3/MinIO; on-node-only backups without Nextcloud upload.
Promotion gate: No further tier-3 repo cutovers until automated daily backups
succeed for 7 consecutive days and one restore drill uses a Nextcloud artifact
(not workstation /tmp).
Current state: Decision adopted; automation not implemented. Restore path proven manually (RAIL-HO-WP-0005-T09, 2026-07-04).
Reference: the-custodian/docs/forgejo-production-decisions.md
8. Credential separation (R3)
Rule: Backup-storage credentials must not live on the production host with permission to delete or overwrite offsite backups.
How this is enforced today
| Control | Mechanism |
|---|---|
| Upload-only file drop | Nextcloud public share token permits PUT only — production host cannot list or delete remote artifacts |
| Separate key material | age private keys stored separately from backup upload tokens |
| OpenBao custody | Offsite lane credentials in platform/workloads/railiance/backup/offsite-lane — not in Git |
| No shared admin | Production k3s/Gitea admin credentials ≠ Nextcloud admin credentials |
Verification steps
| Step | Frequency | Procedure | Owner |
|---|---|---|---|
| CS-1 Token scope | Quarterly | Confirm production backup token cannot DELETE or LIST on Nextcloud drop (curl probe) | Bernd |
| CS-2 Credential inventory | Monthly | Verify no backup-storage admin credentials in /etc, kube Secrets, or Ansible vars on railiance01 |
Bernd |
| CS-3 Separate accounts | Quarterly | Confirm production, backup, registrar, and payment systems use distinct admin identities | Bernd |
| CS-4 Breach runbook | After adoption | Incident playbook requires: never restore into compromised machine; rotate before restore | Bernd |
Known exposure: S1/S2 local backups on railiance01 are deletable by root on that host. Mitigation: wire offsite upload for S1/S2 (tracked in pickup queue).
9. Monitoring and evidence (R9)
Alerting owner: Bernd (until a formal on-call rotation is defined).
Required checks
| Check ID | Check | Frequency | Pass condition | Owner |
|---|---|---|---|---|
| M-1 | Backup job succeeded | Daily | .last-backup stamp < 24h old for each active lane; cron log shows exit 0 |
Bernd |
| M-2 | Artifact freshness | Daily | Latest db-*.sql.age and config-*.tar.gz.age < 24h (dev lane) |
Bernd |
| M-3 | Repository integrity | Weekly | age-decrypt test on one random artifact per lane; file size > 0; pg_dump header valid | Bernd |
| M-4 | Preflight gate | Before infra change | bin/railiance preflight exits 0 |
Bernd |
| M-5 | Restore drill | Quarterly per T1 stream | Dated evidence in TestEvidence/ |
Bernd |
| M-6 | Register review | Monthly | AssetRegister.yaml statuses updated; gaps unchanged or closed |
Bernd |
Preflight gate (mandatory before infrastructure work)
bin/railiance preflight
| Check | Pass condition |
|---|---|
| DB backup freshness | Latest db-*.sql.age < 24 hours old |
| Config backup freshness | Latest config-*.tar.gz.age < 24 hours old |
| Git repos clean | No uncommitted changes in tracked repos |
| Git repos pushed | No unpushed commits |
| age key present | ~/.config/age/railiance-backup.key exists |
Exit 0 = proceed. Exit 1 = do not proceed.
Silent-failure mitigations
- Cron jobs log to
~/.cache/railiance/backup.log— review on M-1 failure. - Backup scripts exit non-zero on any step failure (
set -euo pipefail). - Missing tool (
railiance-platformmake backup) is a policy violation — tracked asbackup-s3-platformstatusmissing. - Forgejo lane not wired — treat as degraded until 7-day promotion gate met.
Evidence output
Every restore drill produces a file under TestEvidence/:
# Restore Drill: <system>
Date, backup artifact used, target environment, result, RTO measured,
RPO measured, issues, follow-up actions.
10. Open gaps (honest)
| Gap | Risk | Remediation | Owner repo |
|---|---|---|---|
S3 railiance-backup tool missing |
Platform DB unprotected | Restore or implement tools/cmd/railiance-backup |
railiance-platform |
| Forgejo automation not wired | RPO = last manual dump | Daily cron + Nextcloud upload per §7 | railiance-platform |
| S1/S2 no offsite upload | Lost if railiance01 destroyed | Extend platform lane upload to S1/S2 artifacts | railiance-platform |
| Gitea no automated backup | Canonical forge at risk | CronJob + mirror; pre-cutover dump runbook | railiance-infra |
apps-pg no ScheduledBackup |
App data unprotected | CNPG ScheduledBackup CR | railiance-platform |
| No immutable weekly offline copy | R3 residual risk | Add offline/append-only copy (Phase 2) | TBD |
| Cold decrypt not evidenced | R5 residual risk | Quarterly cold decrypt drill (M-3 extended) | disaster-control |
Track closure in BackupPickupQueue.md (DISCTL-WP-0003-T04).
11. References
AssetRegister.yaml— stream inventory and statusDataRegister.yaml— data class mappingspecs/ResilienceControlSetup.md— architecture and tier modelhistory/2026-07-04-forgejo-backup-strategy-assessment.md— gap analysishistory/2026-07-04-initial-risk-analysis.md— R3, R9 risk contextrailiance-cluster/docs/backup-restore.md— dev lane operational proceduresthe-custodian/docs/forgejo-production-decisions.md— Option A adoptionrailiance-infra/docs/forgejo-restore-drill-evidence.md— T09 restore proof