Skip to content

Backup and restore

Umbryx stores all durable state in two places: the PostgreSQL volume and the /data directory. Both are required for a working restore.

What to back up

Item Contains Why
PostgreSQL volume (db service) all entities, users, audit events the database
DATA_DIR (default ./data) uploads, loot files (encrypted), audit JSONL mirror, secret.key, admin password file files + generated keys
.env ENCRYPTION_KEY, JWT_SECRET, DB/Redis passwords without ENCRYPTION_KEY the encrypted data is unrecoverable

Procedure

# backup
docker compose exec db pg_dump -U umbryx umbryx | gzip > umbryx-$(date +%F).sql.gz
tar czf umbryx-data-$(date +%F).tar.gz data/            # includes secret.key
cp .env umbryx-env-$(date +%F).backup                    # store securely

# restore (fresh host or after a disaster)
docker compose up -d db
gunzip -c umbryx-<date>.sql.gz | docker compose exec -T db psql -U umbryx umbryx
tar xzf umbryx-data-<date>.tar.gz
cp umbryx-env-<date>.backup .env
docker compose up -d

Keep .env (with the encryption key) in a secret manager, not next to the backups.

Key rotation during restore

If ENCRYPTION_KEY must change, first restore with the old key, then rotate with backend/scripts/rekey.py:

OLD_KEY=<old> NEW_KEY=<new> python3 backend/scripts/rekey.py --dry-run
OLD_KEY=<old> NEW_KEY=<new> python3 backend/scripts/rekey.py   # stop the API first

The script re-encrypts credential secrets, encrypted notes/loot values, loot files and C2 integration tokens in one transaction. Per-project DEKs rotate separately (POST /api/system/encryption/rotate-dek).

Verification after restore

  1. /api/health returns ok
  2. Log in; open a project with known credentials and reveal one secret — decryption with the restored key must succeed
  3. Download a loot file (round-trips through file encryption)
  4. GET /api/admin/audit/verify — integrity report over timeline events (when AUDIT_INTEGRITY_KEY is set)

Failure modes

Symptom Cause
Secrets reveal as decryption errors wrong/lost ENCRYPTION_KEY — unrecoverable by design
Login fails for everyone JWT_SECRET differs from the one that signed sessions — log in again; if the admin password file is lost, reset via DB or first-run path
Loot files download as garbage data volume not restored or key mismatch