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
All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.