Configuration Backup
The Configuration Backup module (id backup, v1.1.0) exports a portable configuration snapshot of your FreeSDN instance into a self-contained .fsdn archive that you can import on any FreeSDN instance. It handles scheduling, eight storage backends, encrypted archives, selective restore, and automatic rollback slots.
What a config snapshot includes and excludes
Section titled “What a config snapshot includes and excludes”The archive is assembled from registered contributors - pluggable units each responsible for one data domain. The CoreBackupContributor covers the domains below. Module-provided contributors add their own domain data.
| Domain | Included by default | Notes |
|---|---|---|
| Organizations and sites | Yes | |
| Controllers (connection config, not credentials) | Yes | Credentials are instance-specific; re-add after import |
| Managed devices | Yes | Toggle with include_devices |
| VLANs and SSIDs | No | Not available in this release - include_vlans / include_ssids are accepted by the API but have no effect in v1.1.0; no VLAN or SSID data is collected or restored |
| Automation rules | Yes | Toggle with include_automation |
| Backup schedules and storage locations | No | Not included in the portable snapshot; reconfigure schedules and storage locations on the destination instance |
| Users | On by default | Pass include_users: false at create time to exclude from the snapshot. restore_users defaults to false - opt in with restore_users: true to restore user records. |
| Encrypted credentials | Never | Fernet-encrypted under the source SECRET_KEY; not portable |
| Audit logs | Never | Instance-tied; use pg-backup |
| Plugin code and install state | Never | |
| Operational telemetry and time-series metrics | Never |
The .fsdn archive format
Section titled “The .fsdn archive format”The archive uses a versioned binary envelope (format version "2.0"):
[4 bytes: header length, big-endian uint][N bytes: JSON header - backup_id, SHA-256 checksum of payload, encrypted flag, compressed flag, created_at, version][remaining: gzip-compressed JSON payload, optionally Fernet-encrypted]The creation pipeline is: collect via contributor registry → gzip → optional Fernet-encrypt → SHA-256 checksum → write header and payload → store via the configured storage backend.
Encryption
Section titled “Encryption”Snapshots are encrypted with Fernet, keyed from a PBKDF2-SHA256 derivation of your instance SECRET_KEY with a random 16-byte per-snapshot salt. PBKDF2 iteration count is 600,000 (OWASP 2025 recommendation). Encryption defaults to on (is_encrypted: true).
The key format is versioned (v2:<iterations>:<base64-salt>). Older v1 archives (100 k iterations, legacy format) are still decryptable; new snapshots always use v2.
Enabling the module
Section titled “Enabling the module”The backup module is always loaded. No additional Compose services are required - it runs inside the existing api and worker containers using the backup PostgreSQL schema. Navigate to Config Backup in the left sidebar.
Creating a snapshot
Section titled “Creating a snapshot”Manual snapshot
Section titled “Manual snapshot”Config Backup → New Snapshot in the UI, or via the API:
POST /api/v1/backups/Content-Type: application/jsonAuthorization: Bearer <token>
{ "name": "pre-migration-2026-06", "backup_type": "full", "include_devices": true, "include_vlans": true, "include_ssids": true, "include_users": false, "include_automation": true, "is_encrypted": true, "storage_location_id": "<uuid>"}The backup runs asynchronously via Celery (soft limit 600 s, hard limit 720 s). Poll status at GET /api/v1/backups/{backup_id}. The status field steps through progress values: 5 → 30 → 50 → 60 → 70 → 90 → 100.
Requires SUPER_ADMIN or ORG_ADMIN role.
Scheduled snapshots
Section titled “Scheduled snapshots”Config Backup → Schedules → New Schedule, or via the API:
POST /api/v1/backups/schedulesContent-Type: application/json
{ "name": "nightly-full", "cron_expression": "0 2 * * *", "storage_location_id": "<uuid>", "retention_days": 30, "max_backups": 10, "is_encrypted": true}| Field | Range | Default | Notes |
|---|---|---|---|
cron_expression | Standard 5-field cron | - | Validated at creation; invalid cron expressions are rejected |
max_backups | 1-1,000 | 10 | When exceeded, the oldest COMPLETED backup is pruned automatically |
retention_days | 1-3,650 | 30 | Snapshots older than this are pruned by the daily cleanup task |
is_encrypted | bool | true | Keep encryption on - snapshots contain cross-org config data |
The Celery beat scheduler (scheduler container) evaluates schedules every 15 minutes using SELECT … FOR UPDATE SKIP LOCKED to prevent double-firing across multiple workers. Missed runs are not replayed - the next eligible slot is chosen, which avoids thundering-herd after downtime.
Toggle a schedule without deleting it:
POST /api/v1/backups/schedules/{schedule_id}/toggleContent-Type: application/json
{ "is_enabled": false }Instant export (no archive)
Section titled “Instant export (no archive)”For a lightweight pfSense-style JSON config download without creating a stored backup record:
GET /api/v1/backups/export?include_devices=true&include_vlans=true&include_ssids=true&include_users=false&include_automation=true&compress=trueResponse is a downloadable file. Requires SUPER_ADMIN or ORG_ADMIN role.
Storage backends
Section titled “Storage backends”Configure storage locations at Config Backup → Storage Settings → New Location, or at POST /api/v1/backups/storage-locations. Storage secrets (API keys, passwords, private keys, tokens) are Fernet-encrypted at rest - they must never be passed in the plaintext config field; the API enforces this with a schema-level guard.
| Backend | storage_type | Notes |
|---|---|---|
| Local filesystem | local | Default base path /data/backups. Path-traversal guard applied. Atomic write (.tmp + rename). |
| NFS | nfs | NFS-mounted paths appear as local; use the same local backend class |
| Amazon S3 or S3-compatible | s3 | boto3; path-style addressing; endpoint host is resolved and pinned to an IP literal at create/test time (DNS-rebind safe) |
| SFTP | sftp | paramiko; host pinned; private_key_path must reside under the sandboxed FREESDN_BACKUP_KEYS_DIR |
| FTP | ftp | ftplib; optional FTPS (use_tls: true); host pinned |
| Google Drive | google_drive | Service-account JSON or OAuth refresh token |
| Dropbox | dropbox | Access token or refresh token + app key |
| WebDAV | webdav | httpx async; base URL resolved and pinned to IP literal; Host header injected for SNI/vhost routing |
Test a storage location
Section titled “Test a storage location”POST /api/v1/backups/storage-locations/{location_id}/testThis triggers a real outbound connectivity check. The endpoint is gated to SUPER_ADMIN or ORG_ADMIN to prevent SSRF probing by lower-privilege users.
List supported backends and config field schemas
Section titled “List supported backends and config field schemas”GET /api/v1/backups/storage-locations/types/supportedReturns the full field schema for each backend type - the same data the frontend uses to render the dynamic configuration form.
Restoring a snapshot
Section titled “Restoring a snapshot”Step 1 - Preview the manifest
Section titled “Step 1 - Preview the manifest”Before restoring, inspect what a snapshot contains without decrypting any live data:
GET /api/v1/backups/{backup_id}/manifestReturns a BackupManifestPreview with per-contributor sections, record counts, and restorability status. Requires SUPER_ADMIN.
Step 2 - Dry run
Section titled “Step 2 - Dry run”POST /api/v1/backups/restoreContent-Type: application/json
{ "backup_id": "<uuid>", "dry_run": true, "restore_devices": true, "restore_vlans": true, "restore_ssids": true, "restore_users": false, "restore_automation": true, "overwrite_existing": false, "target_site_id": null}restore_users defaults to false - you must opt in. overwrite_existing defaults to false - existing rows are left untouched unless you set it to true.
The response is a RestoreJob. Poll GET /api/v1/backups/restore/{job_id} for status and the dry-run report.
Step 3 - Live restore
Section titled “Step 3 - Live restore”Repeat the same request body with dry_run: false. Before applying any writes, the service automatically captures a pre-restore rollback slot (a full backup tagged backup_type=rollback_slot), encrypted and retained for 7 days. If the rollback-slot capture fails, the restore proceeds and the failure is logged - it is best-effort, not blocking.
Rollback after a live restore
Section titled “Rollback after a live restore”To undo a restore, identify the rollback slot linked to the restore job via the rollback_for_restore_job_id field on the BackupResponse and restore from it using POST /api/v1/backups/restore. The Snapshot History list displays a badge on rollback-slot backups so you can find them visually. There is no dedicated “Undo” button in the UI - the mechanism is API-only.
Importing a .fsdn file from another instance
Section titled “Importing a .fsdn file from another instance”POST /api/v1/backups/import?dry_run=true&overwrite_existing=falseContent-Type: multipart/form-data
file=<.fsdn file>Only .fsdn files are accepted - raw JSON and gzip are rejected (they bypass the checksum verification). The upload is size-capped; files over the limit receive HTTP 413. Organization ownership is enforced: rows belonging to a foreign org are rejected. Blocked fields (id, hashed_password, and other privilege-escalation vectors) are stripped on both insert and update paths. Requires SUPER_ADMIN.
API reference (compact)
Section titled “API reference (compact)”All endpoints require an active authenticated session. Org isolation is enforced in the service layer. The storage-locations sub-router is mounted before /{backup_id} to avoid the catch-all path consuming literal sub-paths.
Backups
Section titled “Backups”| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/backups/ | List backups (filter by site_id, backup_type, status, storage_type, search; paginated) | Authenticated |
| POST | /api/v1/backups/ | Create and execute a backup | SUPER_ADMIN / ORG_ADMIN |
| GET | /api/v1/backups/{backup_id} | Get backup detail | Authenticated |
| GET | /api/v1/backups/{backup_id}/download | Download the .fsdn file | SUPER_ADMIN / ORG_ADMIN |
| DELETE | /api/v1/backups/{backup_id} | Delete backup record and storage file | SUPER_ADMIN / ORG_ADMIN |
| GET | /api/v1/backups/{backup_id}/manifest | Preview per-contributor manifest without restoring | SUPER_ADMIN |
| GET | /api/v1/backups/stats | Aggregate stats (site_id filter) | Authenticated |
| GET | /api/v1/backups/export | Instant config download (no stored record) | SUPER_ADMIN / ORG_ADMIN |
| POST | /api/v1/backups/import | Import a .fsdn file (multipart) | SUPER_ADMIN |
| POST | /api/v1/backups/restore | Restore from a backup | SUPER_ADMIN |
| GET | /api/v1/backups/restore/{job_id} | Poll restore job status | Authenticated |
Schedules
Section titled “Schedules”| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/backups/schedules | List schedules | Authenticated |
| POST | /api/v1/backups/schedules | Create schedule | SUPER_ADMIN / ORG_ADMIN |
| GET | /api/v1/backups/schedules/{schedule_id} | Get schedule | Authenticated |
| PUT | /api/v1/backups/schedules/{schedule_id} | Update schedule | SUPER_ADMIN / ORG_ADMIN |
| DELETE | /api/v1/backups/schedules/{schedule_id} | Delete schedule | SUPER_ADMIN / ORG_ADMIN |
| POST | /api/v1/backups/schedules/{schedule_id}/toggle | Enable or disable | SUPER_ADMIN / ORG_ADMIN |
Storage locations
Section titled “Storage locations”| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/backups/storage-locations/types/supported | List backends and config field schemas | Authenticated |
| GET | /api/v1/backups/storage-locations | List configured locations | Authenticated |
| POST | /api/v1/backups/storage-locations | Create location (SSRF-validated) | SUPER_ADMIN / ORG_ADMIN |
| GET | /api/v1/backups/storage-locations/{location_id} | Get one location | Authenticated |
| PATCH | /api/v1/backups/storage-locations/{location_id} | Update location | SUPER_ADMIN / ORG_ADMIN |
| DELETE | /api/v1/backups/storage-locations/{location_id} | Delete location | SUPER_ADMIN / ORG_ADMIN |
| POST | /api/v1/backups/storage-locations/{location_id}/test | Test connectivity | SUPER_ADMIN / ORG_ADMIN |
Automated validation
Section titled “Automated validation”The Celery beat scheduler runs a monthly per-org dry-run restore validation task (backup.validate_restore). It picks the newest COMPLETED backup per organization, runs a full dry_run=True restore (decrypts → verifies SHA-256 → parses → walks restore plan without writing), and records the result.
Each organization is given a 60-second timeout so a single stuck org cannot consume the entire validation budget. Results: ok, warn (no recent backup found), error, or timeout.
Permissions
Section titled “Permissions”| Permission code | Required for |
|---|---|
backup.view | Browse snapshot history and view backup metadata |
backup.create | Trigger manual or on-demand snapshots |
backup.restore | Restore from a snapshot (SUPER_ADMIN enforced at the API layer) |
backup.delete | Delete snapshot records and storage files |
backup.schedule | Create and manage backup schedules |
backup.settings | Configure storage locations and encryption settings |
backup.create, downloading .fsdn files, and managing storage locations require at minimum SUPER_ADMIN or ORG_ADMIN as enforced by the API. backup.restore and backup.import (i.e. POST /api/v1/backups/restore and POST /api/v1/backups/import) require SUPER_ADMIN only - ORG_ADMIN is not sufficient and will receive HTTP 403. The permission codes above describe the RBAC intent, but the restore and import endpoints add a hard SUPER_ADMIN-only role gate regardless of permission assignment. A user with only backup.view can browse snapshot history but will receive HTTP 403 on GET /api/v1/backups/{id}/download.
Fabric integration
Section titled “Fabric integration”| Event type | Severity | When |
|---|---|---|
backup.validation.failed | CRITICAL | Monthly automated dry-run validation failed for one or more orgs |
Wire this to fabric.notify (in-platform notification), a webhook, or an n8n workflow. See Fabric.
Gotchas
Section titled “Gotchas”- Credentials are never exported. After importing a snapshot onto a new instance, re-add all adapter credentials (API keys, passwords) via Settings → Credentials before running discovery or staging writes.
- Missed schedule runs are not replayed. After the scheduler comes back up from downtime it picks the next eligible slot forward, not the missed windows. This is intentional to prevent a burst of simultaneous backups.
- Schema version mismatches on restore. If a contributor’s data was written with a different major schema version, it is skipped with status
schema_mismatchunless the contributor implements aMigratingContributormigration path. Check the manifest preview before restoring across major version gaps. - Rollback-slot capture is best-effort. The pre-restore snapshot is attempted but a failure is logged-and-skipped rather than blocking the restore. Verify that a rollback slot was created before assuming you can undo.
restore_usersdefaults to false. User records are not restored unless you explicitly opt in. This is a deliberate default to prevent overwriting accounts on the destination instance.- Decompression bomb guard. The import endpoint uses bounded decompression with a hard byte ceiling. Files that decompress beyond
MAX_BACKUP_IMPORT_BYTESreceive HTTP 413. - Not DR. If PostgreSQL is corrupted or lost, the
.fsdnsnapshot cannot recover audit-log history, time-series metrics, or operational telemetry. For that you need the PostgreSQL dump from thepg-backupcontainer plus thedrCompose profile.
Next steps
Section titled “Next steps”- Set up off-site DR: Backups and Restore
- Wire
backup.validation.failedto notify your team: Fabric - Understand deployment tiers and the
drCompose profile: Deployment - Review the adapter credential model before importing onto a new instance: Adapters