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¶
/api/healthreturnsok- Log in; open a project with known credentials and reveal one secret — decryption with the restored key must succeed
- Download a loot file (round-trips through file encryption)
GET /api/admin/audit/verify— integrity report over timeline events (whenAUDIT_INTEGRITY_KEYis 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 |