Skip to content

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.

DomainIncluded by defaultNotes
Organizations and sitesYes
Controllers (connection config, not credentials)YesCredentials are instance-specific; re-add after import
Managed devicesYesToggle with include_devices
VLANs and SSIDsNoNot 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 rulesYesToggle with include_automation
Backup schedules and storage locationsNoNot included in the portable snapshot; reconfigure schedules and storage locations on the destination instance
UsersOn by defaultPass 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 credentialsNeverFernet-encrypted under the source SECRET_KEY; not portable
Audit logsNeverInstance-tied; use pg-backup
Plugin code and install stateNever
Operational telemetry and time-series metricsNever

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.

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.

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.

Config Backup → New Snapshot in the UI, or via the API:

POST /api/v1/backups/
Content-Type: application/json
Authorization: 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.

Config Backup → Schedules → New Schedule, or via the API:

POST /api/v1/backups/schedules
Content-Type: application/json
{
"name": "nightly-full",
"cron_expression": "0 2 * * *",
"storage_location_id": "<uuid>",
"retention_days": 30,
"max_backups": 10,
"is_encrypted": true
}
FieldRangeDefaultNotes
cron_expressionStandard 5-field cron-Validated at creation; invalid cron expressions are rejected
max_backups1-1,00010When exceeded, the oldest COMPLETED backup is pruned automatically
retention_days1-3,65030Snapshots older than this are pruned by the daily cleanup task
is_encryptedbooltrueKeep 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}/toggle
Content-Type: application/json
{ "is_enabled": false }

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=true

Response is a downloadable file. Requires SUPER_ADMIN or ORG_ADMIN role.

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.

Backendstorage_typeNotes
Local filesystemlocalDefault base path /data/backups. Path-traversal guard applied. Atomic write (.tmp + rename).
NFSnfsNFS-mounted paths appear as local; use the same local backend class
Amazon S3 or S3-compatibles3boto3; path-style addressing; endpoint host is resolved and pinned to an IP literal at create/test time (DNS-rebind safe)
SFTPsftpparamiko; host pinned; private_key_path must reside under the sandboxed FREESDN_BACKUP_KEYS_DIR
FTPftpftplib; optional FTPS (use_tls: true); host pinned
Google Drivegoogle_driveService-account JSON or OAuth refresh token
DropboxdropboxAccess token or refresh token + app key
WebDAVwebdavhttpx async; base URL resolved and pinned to IP literal; Host header injected for SNI/vhost routing
POST /api/v1/backups/storage-locations/{location_id}/test

This 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/supported

Returns the full field schema for each backend type - the same data the frontend uses to render the dynamic configuration form.

Before restoring, inspect what a snapshot contains without decrypting any live data:

GET /api/v1/backups/{backup_id}/manifest

Returns a BackupManifestPreview with per-contributor sections, record counts, and restorability status. Requires SUPER_ADMIN.

POST /api/v1/backups/restore
Content-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.

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.

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=false
Content-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.

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.

MethodPathPurposeAuth
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 backupSUPER_ADMIN / ORG_ADMIN
GET/api/v1/backups/{backup_id}Get backup detailAuthenticated
GET/api/v1/backups/{backup_id}/downloadDownload the .fsdn fileSUPER_ADMIN / ORG_ADMIN
DELETE/api/v1/backups/{backup_id}Delete backup record and storage fileSUPER_ADMIN / ORG_ADMIN
GET/api/v1/backups/{backup_id}/manifestPreview per-contributor manifest without restoringSUPER_ADMIN
GET/api/v1/backups/statsAggregate stats (site_id filter)Authenticated
GET/api/v1/backups/exportInstant config download (no stored record)SUPER_ADMIN / ORG_ADMIN
POST/api/v1/backups/importImport a .fsdn file (multipart)SUPER_ADMIN
POST/api/v1/backups/restoreRestore from a backupSUPER_ADMIN
GET/api/v1/backups/restore/{job_id}Poll restore job statusAuthenticated
MethodPathPurposeAuth
GET/api/v1/backups/schedulesList schedulesAuthenticated
POST/api/v1/backups/schedulesCreate scheduleSUPER_ADMIN / ORG_ADMIN
GET/api/v1/backups/schedules/{schedule_id}Get scheduleAuthenticated
PUT/api/v1/backups/schedules/{schedule_id}Update scheduleSUPER_ADMIN / ORG_ADMIN
DELETE/api/v1/backups/schedules/{schedule_id}Delete scheduleSUPER_ADMIN / ORG_ADMIN
POST/api/v1/backups/schedules/{schedule_id}/toggleEnable or disableSUPER_ADMIN / ORG_ADMIN
MethodPathPurposeAuth
GET/api/v1/backups/storage-locations/types/supportedList backends and config field schemasAuthenticated
GET/api/v1/backups/storage-locationsList configured locationsAuthenticated
POST/api/v1/backups/storage-locationsCreate location (SSRF-validated)SUPER_ADMIN / ORG_ADMIN
GET/api/v1/backups/storage-locations/{location_id}Get one locationAuthenticated
PATCH/api/v1/backups/storage-locations/{location_id}Update locationSUPER_ADMIN / ORG_ADMIN
DELETE/api/v1/backups/storage-locations/{location_id}Delete locationSUPER_ADMIN / ORG_ADMIN
POST/api/v1/backups/storage-locations/{location_id}/testTest connectivitySUPER_ADMIN / ORG_ADMIN

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.

Permission codeRequired for
backup.viewBrowse snapshot history and view backup metadata
backup.createTrigger manual or on-demand snapshots
backup.restoreRestore from a snapshot (SUPER_ADMIN enforced at the API layer)
backup.deleteDelete snapshot records and storage files
backup.scheduleCreate and manage backup schedules
backup.settingsConfigure 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.

Event typeSeverityWhen
backup.validation.failedCRITICALMonthly 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.

  • 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_mismatch unless the contributor implements a MigratingContributor migration 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_users defaults 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_BYTES receive HTTP 413.
  • Not DR. If PostgreSQL is corrupted or lost, the .fsdn snapshot cannot recover audit-log history, time-series metrics, or operational telemetry. For that you need the PostgreSQL dump from the pg-backup container plus the dr Compose profile.
  • Set up off-site DR: Backups and Restore
  • Wire backup.validation.failed to notify your team: Fabric
  • Understand deployment tiers and the dr Compose profile: Deployment
  • Review the adapter credential model before importing onto a new instance: Adapters