4c388a398b
gates / gates (push) Successful in 5m41s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
5.9 KiB
5.9 KiB
Runbook — bring Gitea back from the off-site copy on ep0 (R-232)
TESTED 2026-10-09 into a throwaway (the bench, LXC 9401 on demo-hp): 10 of 10 repositories listed, the four product repositories'
mainequal to live Gitea (one was one commit behind: that commit was pushed three minutes after the copy, and the copy's commit is its parent), one file byte for byte, a throwaway admin logged in. Restore from ep0: 544 MB in 22 s. Evidence:audits/dooplex-survival-2026-10-09/partC/. The copy itself:audits/dooplex-survival-2026-10-09/PLAN.mdandscripts/dooplex-offsite/.
What the copy holds
One encrypted archive dooplex.pxar per night in ep0's PBS, namespace operator, group host/dooplex-gitea
(14 daily + 8 weekly kept). Inside:
| Path | What |
|---|---|
db/gitea.dump |
pg_dump -Fc of the gitea database (PostgreSQL 17.2), taken BEFORE the files |
db/globals.sql, db/DUMP-FOLDER |
all roles of the CNPG cluster (password hashes — not needed for this restore); which dump |
gitea/git/repositories/<owner>/<repo>.git |
the bare repositories |
gitea/git/lfs, gitea/gitea/{attachments,avatars,repo-avatars,jwt} |
the rest of Gitea's data |
gitea/gitea/conf/app.ini |
the config, with Gitea's secrets (SECRET_KEY, INTERNAL_TOKEN, JWT, the DB password) |
secrets/*.gpg |
DooPlex's nightly k8s Secrets/ConfigMaps export, GPG-encrypted with DooPlex's restic passphrase |
MANIFEST.sha256, REPOS |
a checksum of every file; the repository count |
Not in it: the container registry (/data/gitea/packages, 27.7 GB). The images rebuild from the code.
What you need
- The key: the
datafield of the paper key — S1 on the break-glass sheet (break-glass-sheet.md). Not the password manager: it runs on DooPlex and is itself inside this copy (R-923). Write{"kdf": null, "created": "2026-01-01T00:00:00+00:00", "modified": "2026-01-01T00:00:00+00:00", "data": "<data>"}toenc.key(root,umask 077). On DooPlex it is/etc/felhom-dooplex-offsite/enc.key. - A read-only token for
dooplex-hub@pbs!restore(DooPlex:/etc/felhom-hub-backup/token-restore), or ep0 root to mint one (RUNBOOK-hub-db-offsite-backup.mdStep 2). - A route to ep0's PBS (
127.0.0.1:18007through DooPlex's tunnel, or ep0's 8007 over the WireGuard). - A machine with Docker. For the secrets files: DooPlex's restic passphrase (operator, offline).
Steps
- Restore the newest copy (any machine with
proxmox-backup-client):export PBS_PASSWORD_FILE=<token-restore file> PBS_FINGERPRINT=<ep0 cert fingerprint, /etc/felhom-hub-backup/env> R='dooplex-hub@pbs!restore@<ep0 PBS>:felhom-offsite' proxmox-backup-client snapshot list host/dooplex-gitea --ns operator --repository "$R" # pick the newest umask 077; proxmox-backup-client restore host/dooplex-gitea/<time> dooplex.pxar ./out --ns operator --keyfile enc.key --repository "$R" (cd out && sha256sum -c MANIFEST.sha256 >/dev/null && echo manifest OK) - The database.
pg_restoremust be 17 (the dump is from 17.2):On a rebuilt DooPlex: restore into the CNPG cluster'sdocker network create --internal gr-net # a test: no route out. A real rebuild: a normal network docker run -d --name gr-db --network gr-net -e POSTGRES_PASSWORD=<pw> postgres:17.2 docker exec -i gr-db psql -U postgres -c "CREATE ROLE gitea LOGIN PASSWORD '<gitea pw>'" -c "CREATE DATABASE gitea OWNER gitea" docker cp out/db/gitea.dump gr-db:/tmp/ && docker exec gr-db pg_restore -U postgres -d gitea --no-owner --role=gitea --exit-on-error /tmp/gitea.dumpgiteadatabase instead (samepg_restoreline). - The config. In
out/gitea/gitea/conf/app.ini,[database]:HOST→ the new database,PASSWD→<gitea pw>. For a test also[mailer] ENABLED = false. Keep every other key —SECRET_KEYandINTERNAL_TOKENmust be the old ones or Gitea cannot read its own stored secrets (2FA, tokens). - Start Gitea on the data, owned by uid 1000 (the image's
gituser):chown -R 1000:1000 out/gitea docker run -d --name gr-gitea --network gr-net -v "$PWD/out/gitea:/data" gitea/gitea:<the version live ran> docker exec gr-gitea wget -q -O - http://127.0.0.1:3000/api/healthz # "status": "pass" - Check it (a throwaway admin for a test; the real admin's password works on a real rebuild):
docker exec -u git gr-gitea gitea admin user create --admin --username restore-check --password <pw> \ --email restore-check@example.invalid --must-change-password=false # /api/v1/repos/search?limit=50&private=true → the count equals out/REPOS # /api/v1/repos/admin/<repo>/branches/main → equals the last known main (git ls-remote of any clone) # /api/v1/repos/admin/felhom.eu/raw/CLAUDE.md?ref=<main> | sha256sum → equals `git show <main>:CLAUDE.md | sha256sum` - A test ends with teardown — the copy holds Gitea's secrets:
Never
docker rm -f gr-gitea gr-db; docker network rm gr-net; docker rmi postgres:17.2 gitea/gitea:<ver> docker volume ls # ⚠ postgres leaves an ANONYMOUS volume holding the restored database — remove it BY NAME find out -type f \( -name app.ini -o -name gitea.dump -o -name globals.sql -o -name '*.gpg' \) -exec shred -u {} +; rm -rf out enc.keydocker volume prune: on a shared machine it deletes other volumes too.
Gotchas found on 2026-10-09
- The anonymous Postgres volume survives
docker rm -f(no-v). It held the restored database; found by counting volumes after the teardown, removed by name. pg_restore --no-owner --role=gitea: the dump's objects belong togiteain the source too, but--no-owneravoids needing every role fromglobals.sql.- The weekly restore test on DooPlex (
felhom-dooplex-offsite-restore-test, Sun 05:30) checks the manifest,git fsckon every repository andpg_restore --list— it does not start Gitea. This runbook is the full test.