Backup & Recovery
DFIRe includes an integrated backup system that creates encrypted database snapshots stored on your configured storage backend. Backups can be created manually, run on a schedule, downloaded for offsite storage, and restored through the web interface or command line.
The integrated backup is a second-tier convenience feature — not your primary backup strategy.
Treat the integrated backup as a soft rollback tool for undoing mistakes (bad config change, an unwanted mass edit) when rolling back in minutes matters more than engaging your full infrastructure recovery process. It is not a substitute for the backups provided by your deployment platform. Your primary backup strategy should continue to be whichever approach is best suited to your deployment model, for example:
- VM snapshots of the host running DFIRe (hypervisor, cloud-provider volume snapshots, etc.)
- Managed database backups provided by your PostgreSQL host (RDS automated snapshots, DigitalOcean / Supabase managed backups, etc.) or scheduled
pg_dumpagainst a direct connection - Container volume backups — whatever mechanism captures the PostgreSQL data volume, Redis volume, and attachment storage on your platform
- Object storage versioning / replication on your S3 bucket or equivalent, if attachments live there
Do not disable these because DFIRe ships an integrated backup. The integrated backup runs inside the same process and on the same server as DFIRe itself; if the host is lost, corrupted, or compromised, the integrated backups on it may be lost or compromised with it. Platform-level and infrastructure-level backups are the ones that save you from a bad day.
Overview
The backup system is managed in Settings > Backup & Recovery and requires superuser access. It provides:
- Encrypted snapshots — Full database dumps encrypted with AES-256-GCM, derived from your
SECRET_KEY - Scheduled backups — Configurable frequency (hourly to weekly), time of day, and automatic retention management
- Storage integration — Backups are written to whichever storage backend is active (Local, S3, or SMB)
- Download & upload — Download backup files for offsite storage and upload them back into any DFIRe instance that shares the same
SECRET_KEY - Web restore — One-click restore with session management and a blocking overlay for all connected users
- CLI tools — Management commands for scripted or offline backup, restore, and forensic extraction
What Is Included in a Backup
Each backup is a complete PostgreSQL database dump containing:
- All cases, evidence items, notes, timers, and investigation reports
- Indicators of compromise, enrichment data, and TAXII/MISP configuration
- User accounts, roles, permissions, and API keys
- Audit logs (a full forensic record of system activity up to the backup point)
- System configuration: workflow steps, playbooks, evidence types, runbooks, webhooks, and tenant settings
- Timeline events, chain-of-custody records, and compliance timer state
- File attachment metadata and encryption keys (needed to decrypt files on storage)
Attachment files are not included in the backup. Encrypted attachment files remain on the storage backend (Local, S3, or SMB). The backup contains the encryption keys and metadata needed to access them. Both the database backup and the storage files are needed for a complete recovery.
Encryption
Backup files are encrypted at rest using a key derived from your Django SECRET_KEY:
- Key derivation: PBKDF2-HMAC-SHA256 with 600,000 iterations and a random 16-byte salt per backup
- Encryption: AES-256-GCM with 8 MB streaming chunks, each independently authenticated. The 32-byte file header is bound into every chunk as additional authenticated data (AAD), so any modification to the magic bytes, format version, KDF salt, or reserved header bytes invalidates every chunk's authentication tag.
- File format: A 32-byte header (magic bytes, format version, KDF salt), followed by an encrypted metadata chunk, followed by the encrypted database dump in 8 MB chunks, terminated by an encrypted end-of-file sentinel. The sentinel is how truncation is detected — a file cut at any chunk boundary would still verify chunk-by-chunk, but the missing sentinel makes the truncation fatal at decrypt time.
Backup files use the .enc extension and can only be decrypted with the SECRET_KEY that created them.
Your SECRET_KEY is the master key to your backups. If you lose the SECRET_KEY, backup files cannot be decrypted. Store your .env file (containing SECRET_KEY and CREDENTIAL_ENCRYPTION_KEY) in a secure location separate from the backup files — a password manager, secrets manager, or secure offline storage.
Creating Backups
Command-line convention. The docker compose examples on this page assume an install.sh-created production deployment and run from the install directory (e.g., /opt/dfire). You need to include the compose file(s) with -f:
- Standard install (internal PostgreSQL container):
docker compose -f docker-compose.prod.yml exec backend python manage.py <command> - External-database install (DATABASE_URL points at an external PostgreSQL):
docker compose -f docker-compose.prod.yml -f docker-compose.external-db.yml exec backend python manage.py <command>
Examples below use the standard-install form for brevity — add -f docker-compose.external-db.yml after the first -f flag if you use the external-DB overlay.
Manual Backup (Web Interface)
In Settings > Backup & Recovery, click Create Backup Now. The backup runs as a background task — the page will update automatically when it completes. You can continue using DFIRe while the backup is running.
Manual Backup (Command Line)
For scripted or offline use, the create_backup management command runs synchronously without requiring the Django-Q2 task queue:
docker compose -f docker-compose.prod.yml exec backend python manage.py create_backup
docker compose -f docker-compose.prod.yml exec backend python manage.py create_backup --description "Before v1.3.0 upgrade"
The command outputs the backup UUID, storage path, file size, and checksum when complete.
Scheduled Backups
Enable scheduled backups in the Scheduled Backups section of the Backup & Recovery settings. Configure:
- Frequency — Every hour, every 6 hours, every 12 hours, daily, every 2 days, or weekly
- Time — For daily, every-2-day, and weekly schedules, choose the time of day (24-hour format)
- Day — For weekly schedules, choose the day of the week
- Retention count — Number of backups to keep; older backups are automatically deleted
After saving, run the setup_schedules management command to activate the schedule in the Django-Q2 task queue:
docker compose -f docker-compose.prod.yml exec backend python manage.py setup_schedules
The schedule summary (e.g., "Every Saturday at 23:00") is displayed below the configuration controls.
Managing Backups
The backup list shows all available backups with their date, size, type (manual or scheduled), and DFIRe version. The list is synchronized from storage on every load and refresh — it reflects the actual files on the storage backend, not just database records.
For each backup you can:
- Verify — Fully decrypt every chunk of the backup file, checking each AES-GCM authentication tag and the trailing EOF sentinel, and compare the resulting SHA-256 against the stored sidecar checksum. A verify pass that completes without errors proves the file is intact, untampered, not truncated, and decryptable with the running instance's
SECRET_KEY. The result panel shows the DFIRe version, creation date, storage location, chunk count, and SHA-256. - Download — Stream-download the encrypted
.encfile for offsite or offline storage. - Restore — Replace the current database with the backup contents. See Restoring from a Backup below.
- Delete — Remove the backup file from storage and its database record.
Upload a Backup
Click Upload Backup to import a previously downloaded .enc file. The upload endpoint fully decrypts every chunk and verifies the EOF sentinel before the backup record is persisted — a file encrypted with a different SECRET_KEY, truncated at any chunk boundary, bit-flipped in any chunk, or modified in the header is rejected with a 400 error and no database record is created. This is deliberate: an upload that returns success has already passed the same verification pass the restore path will run, so a surprise "this backup is actually broken" discovery at restore time is prevented. After a successful upload, the backup appears in the list and can be restored normally.
Refresh
The Refresh button performs a full synchronization: it scans the storage backend for .enc files, removes database records for files that no longer exist, and creates records for any files found on storage. This is useful after changing storage backends, after a restore (which may leave stale records from the restored database), or if backup files were added or removed outside of DFIRe.
Listing Backups (Command Line)
The list_backups management command prints a formatted table of all backups (UUID, creation date, version, size, type, status, and restore status). It runs sync_from_storage first by default, so the output reflects the files actually present on the storage backend:
docker compose -f docker-compose.prod.yml exec backend python manage.py list_backups
docker compose -f docker-compose.prod.yml exec backend python manage.py list_backups --no-sync # skip sync
docker compose -f docker-compose.prod.yml exec backend python manage.py list_backups --json # machine-readable
This is the intended way to discover a backup UUID before running restore_backup from the command line.
Restoring from a Backup
Restore is destructive. It drops and rebuilds the entire database. All current data — cases, users, configuration — is replaced with the backup snapshot. This cannot be undone. Consider creating a backup of the current state before restoring.
Web Restore
Click the restore icon on any completed backup. A confirmation dialog requires you to acknowledge the destructive nature of the operation. When confirmed:
- All other user sessions are immediately revoked (force-logout)
- A full-screen blocking overlay appears for any connected users, including the initiating administrator
- The database is dropped and rebuilt from the backup using
pg_restore - When the restore completes, the overlay redirects all users to the login page
The restore runs as a background task. If you close the browser during the restore, the operation continues — reload the page to see the overlay with current status.
Command-Line Restore
For disaster recovery scenarios where the web interface may not be available:
# Interactive (prompts for confirmation)
docker compose -f docker-compose.prod.yml exec backend python manage.py restore_backup <backup-uuid>
# Non-interactive (for scripts)
docker compose -f docker-compose.prod.yml exec -T backend python manage.py restore_backup <backup-uuid> --yes
The command validates the backup before proceeding, displays metadata (version, creation date, PostgreSQL version), and requires typing RESTORE to confirm unless --yes is specified.
After Restore
After a restore completes:
- All users must log in again (sessions were revoked)
- The backup list may show stale entries from the restored database — click Refresh to resynchronize from storage
- A post-restore audit log entry is written as a demarcation point between restored and new activity
- You may need to restart application containers if the restore changed Django-Q2 task state
Database Connection
Connection poolers are fine for normal application traffic. DFIRe is tested and supported in production against PgBouncer (transaction pooling) and equivalent poolers on managed platforms. The application's regular read/write workload multiplexes across pooled connections safely.
Backup and restore are the exception. pg_dump and pg_restore must speak to PostgreSQL directly. Running them through a connection pooler (PgBouncer, pgcat, RDS Proxy, DigitalOcean managed pooler, Supabase, etc.) will permanently corrupt the database on restore, because object IDs and session-scoped state from pg_restore's parallel streams get multiplexed across different backend connections. This is irreversible.
Never point DFIRe's backup/restore tools at a connection pooler. A pooled backup may appear to succeed while producing an unrestorable file; a pooled restore will silently corrupt the target database. DFIRe does not attempt to autodetect poolers — detection turned out to be unreliable across managed database providers — so the operator must declare the connection mode explicitly before any backup or restore is allowed. If your primary DATABASE_URL points at a pooler, a separate direct database URL is required for backup and restore; responsibility for providing a direct URL that bypasses the pooler is entirely on you.
Declaring the Connection Mode
In Settings > Backup & Recovery > DFIRe Server Connection Mode, choose one of two modes. Backup and restore controls remain disabled until a mode is saved.
Direct database connection
Choose this when DFIRe's DATABASE_URL already points straight at PostgreSQL. No separate URL is needed — pg_dump and pg_restore will reuse the same connection string.
Before this mode becomes active you must tick an acknowledgement checkbox confirming that the configured host is a real PostgreSQL endpoint and not a pooler. The checkbox exists because the consequence of getting this wrong is database corruption on restore, and the system has no reliable way to verify the claim for you.
Connection pool (e.g. PgBouncer)
Choose this when DFIRe's DATABASE_URL points at a pooler. A separate Direct Database URL is then required — it is used only by pg_dump and pg_restore, while the rest of the application keeps using the pooled URL. The direct URL follows the standard PostgreSQL format:
postgres://username:password@db-host:5432/database_name
After saving the direct URL, click Test Connection to verify DFIRe can reach PostgreSQL through it and that the resulting pg_dump client version is compatible with the server. The test does not attempt to classify the URL as direct or pooled — it is the operator's responsibility to provide a URL that bypasses the pooler.
Check with your platform provider if you are unsure whether an endpoint is a pooler. Common tells: the hostname contains pooler, pgbouncer, or proxy; the port is 6432, 25061, or another non-standard value; the provider's docs describe the endpoint as "transaction mode" or "shared connection". A direct PostgreSQL endpoint typically uses port 5432 and its hostname references the database instance itself.
Switching Modes
Switching from Direct to Pool clears the acknowledgement checkbox; switching from Pool to Direct clears the stored direct database URL. In both cases the new mode must be fully configured and saved before backups or restores can run again.
PostgreSQL Version Compatibility
DFIRe ships with PostgreSQL client tools for versions 16, 17, and 18. The correct version is selected automatically based on your database server version. The client version must be equal to or greater than the server version — a newer client can back up from an older server, but not the reverse.
Forensic Extraction
The decrypt_backup management command decrypts a .enc file into a raw PostgreSQL custom-format dump. This is useful for forensic analysis, auditing, or loading the data into a separate PostgreSQL instance for inspection.
# Decrypt using the running instance's SECRET_KEY
docker compose -f docker-compose.prod.yml exec backend python manage.py decrypt_backup /path/to/backup.enc \
-o /tmp/decrypted.dump
# Decrypt with an explicit SECRET_KEY (offline extraction)
python manage.py decrypt_backup /path/to/backup.enc \
-o /tmp/decrypted.dump --secret-key "your-secret-key-here"
# View only the metadata (no dump output)
python manage.py decrypt_backup /path/to/backup.enc -o /dev/null --metadata-only
The output .dump file is a standard pg_dump --format=custom archive. You can inspect it or load it into any PostgreSQL database:
# List tables and objects in the dump
pg_restore --list /tmp/decrypted.dump
# Restore into a separate database for analysis
pg_restore -d analysis_db /tmp/decrypted.dump
The decrypted dump contains sensitive data including user credentials, API keys, and file attachment encryption keys. Handle the output file with the same care as the live database. Delete it securely when finished.
Secret Key Backup
Two secret keys in your .env file are critical for recovery. Store them in a secure location separate from your data backups:
| Key | Purpose |
|---|---|
SECRET_KEY |
Used to derive backup encryption keys, Django session management, and CSRF protection. Required to decrypt any backup file. |
CREDENTIAL_ENCRYPTION_KEY |
Encrypts stored credentials (integration API keys, webhook secrets, SSO configuration). Required to access integrations after restore. |
Recommended storage locations:
- Password manager — Your organization's password manager (1Password, Bitwarden, etc.)
- Secrets manager — A dedicated secrets management solution (HashiCorp Vault, AWS Secrets Manager, etc.)
- Secure offline storage — Printed and stored in a secure physical location as a last-resort backup
Never regenerate these keys on an existing installation. Changing the SECRET_KEY makes all existing backups undecryptable. Changing the CREDENTIAL_ENCRYPTION_KEY makes stored credentials unreadable.
File Attachments and the Database
File attachment encryption keys are stored in the database, not in your environment file. Loss of the database means permanent loss of access to all file attachments — even if the encrypted files themselves are intact on storage.
This coupling between the database and file storage means:
- Database backups are essential — Without the database, encrypted files cannot be decrypted
- Database and storage should be backed up together — A database backup without the corresponding files (or vice versa) is an incomplete recovery set
- Point-in-time consistency matters — Files uploaded after a backup was created will not have their encryption keys in that backup
External Backup Responsibilities
DFIRe's integrated backups cover the database. You are responsible for backing up the external services you provide:
| Component | Your Responsibility |
|---|---|
| File Storage | Back up according to your storage solution: S3 versioning and cross-region replication, NAS snapshots, or file server backups. DFIRe backups do not include the encrypted attachment files. |
| Environment File | Keep a secure copy of your .env file containing SECRET_KEY, CREDENTIAL_ENCRYPTION_KEY, and database connection strings. |
Application Configuration Export
DFIRe can export application configuration (playbooks, evidence types, workflow steps, webhooks, permission groups, enrichment providers, and similar reference data) as a JSON file. This is a lightweight way to replicate settings across installations without touching case data.
Two export modes are available:
- Plaintext export (default). Produces a human-readable JSON file. Secrets are skipped — API keys, webhook tokens, SSO client secrets, and similar credentials are not included. Use this for version-controlled configuration templates or for moving non-sensitive settings between environments.
- Encrypted export. Produces the same JSON payload with secrets included (API keys, webhook tokens, SSO client secrets), symmetrically encrypted with a password you set at export time. DFIRe does not store the password — if you lose it, the export cannot be decrypted. Use this when you want a portable bundle that, together with its password, fully reconstructs a DFIRe instance's configuration on another host. Treat the password with the same care as
SECRET_KEY.
- Go to Settings > Global Settings > Identity
- In the Data Portability section, choose Export JSON (plaintext) or Export JSON (encrypted)
- For an encrypted export, set a strong password. DFIRe will not show or store it — record it in your password manager before dismissing the dialog.
- Save the exported file securely
Encrypted exports are the only configuration-export mode that contains secrets. Never share an encrypted export together with its password — transfer them through separate channels, and rotate affected credentials if you suspect either has been exposed.
Disaster Recovery Walkthrough
To restore DFIRe to a new environment from an encrypted backup file:
- Deploy a fresh DFIRe instance using your backed-up
.envfile (the originalSECRET_KEYis required) - Configure the storage backend in Settings > Storage to point to the same storage location (or restore your storage files first)
- Declare the connection mode in Settings > Backup & Recovery. Backup and restore are disabled until this is saved — see Database Connection above.
- Upload the backup file via Settings > Backup & Recovery > Upload Backup. The system fully decrypts and verifies every chunk before accepting it; a file encrypted with a different
SECRET_KEY, or one that has been truncated or tampered with, is rejected. - Restore by clicking the restore icon on the uploaded backup
- Verify that cases, evidence, file attachments, and user accounts are accessible
Alternatively, if you have CLI access, use the management commands:
# 1. Upload the backup file to the storage backend's backups/ directory manually,
# or use the upload endpoint
# 2. List available backups (runs sync_from_storage first, then prints a table
# with UUIDs, creation dates, versions, sizes and status)
docker compose -f docker-compose.prod.yml exec backend python manage.py list_backups
# 3. Restore
docker compose -f docker-compose.prod.yml exec backend python manage.py restore_backup <backup-uuid> --yes