Backup & migration
Three workflows live in the same place: JSON config backup & restore for day-to-day snapshots, OPNsense / pfSense XML import for migrating off another firewall, and commit-confirm for safe remote edits that auto-revert if you lose access. Versioned history sits underneath all three so any change is one click away from a rollback.
JSON backup & restore
Export the entire configuration — rules, NAT, aliases, VPN, geo-IP, DNS, DHCP, IDS settings, multi-WAN policy, reverse proxy, users, OAuth providers — as a single JSON document:
curl https://aifw.local/api/v1/config/export \
-H "Authorization: Bearer $TOKEN" \
-o aifw-backup-$(date +%F).json
Restore by POSTing the same document back. The importer validates the shape, snapshots the current config to history first, then applies the new state through the same engine writers (RuleEngine, NatEngine, AliasEngine, …) that the UI uses — so pf actually picks up the changes.
curl -X POST https://aifw.local/api/v1/config/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @aifw-backup.json
Preview a candidate import without applying via POST /api/v1/config/import-preview. The response lists every record that will be created / updated / deleted so you can sanity-check the diff first.
S3 backup destination
Push backups off-box on a schedule. Configure a bucket, region, prefix, and access keys; AiFw uploads a fresh JSON snapshot on every config save plus on a configurable rotation schedule.
curl -X PUT https://aifw.local/api/v1/backup/s3/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"bucket": "ops-aifw-backups",
"region": "us-east-1",
"prefix": "site-a/",
"path_style": false,
"access_key_id": "AKIA...",
"secret_access_key": "..."
}'
# Verify credentials + bucket access without touching anything:
curl -X POST https://aifw.local/api/v1/backup/s3/test \
-H "Authorization: Bearer $TOKEN"
# Restore a specific snapshot:
curl https://aifw.local/api/v1/backup/s3/list \
-H "Authorization: Bearer $TOKEN"
curl -X POST https://aifw.local/api/v1/backup/s3/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "site-a/2026-05-08T03:15:22Z.json"}'
S3-compatible endpoints work too — set endpoint and path_style: true for MinIO, Backblaze B2, Wasabi, Cloudflare R2, ….
OPNsense import
Migrating from OPNsense or pfSense? Drop the appliance’s config.xml straight into AiFw. The importer was rewritten in #230 and hardened across #248–#252 with a documented atomicity model and round-trip rollback test coverage.
Atomicity model
- Parse first.
quick-xmlwalks the document. Every error that can be detected from the XML alone (malformed source/destination, unknown action, unparseable port range) surfaces in the preview — no half-applied state. - Preview the diff.
POST /api/v1/config/preview-opnsensereturns aliases, NAT entries, rules, and static routes that will be created, plus a skipped list for rules that referenced network keywords (lan,wanip,(self), …) that don’t map cleanly. AiFw drops those to skipped instead of guessing — silent fidelity loss is the worst outcome. - Apply atomically.
POST /api/v1/config/import-opnsenseruns the apply step inside a SQLite transaction that wraps every engine write. Aliases route throughAliasEngine, NAT throughNatEngine, rules throughRuleEngine, static routes through the sameapply_route_to_systempath the manual REST endpoint uses, and DNS forwarders through the same path the rDNS resolver UI writes — not/etc/resolv.conf(see #231). - Snapshot & commit-confirm. Before any write the importer saves a
pre-OPNsense-importconfig-history version, and after a successful apply it arms commit-confirm with a 600-second timeout. If the admin loses access to the appliance before they confirm, AiFw rolls back automatically. - Reject → BlockReturn. OPNsense’s
rejectaction maps to AiFwAction::BlockReturn, which produces the same per-protocol response (TCP RST, ICMP unreachable for UDP/ICMP) the original ruleset emitted.
Walkthrough (UI)
Backup & restore → OPNsense import → Upload config.xml → review preview (aliases, NAT, rules, skipped list, interface map) → Apply → Confirm within the commit-confirm window.
Walkthrough (API)
# 1) Preview
curl -X POST https://aifw.local/api/v1/config/preview-opnsense \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"xml": "'"$(base64 -w0 < /tmp/config.xml)"'",
"interface_map": { "wan": "em0", "lan": "em1" }
}'
# 2) Apply (uses same body shape; commit-confirm starts on success)
curl -X POST https://aifw.local/api/v1/config/import-opnsense \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @import-body.json
# 3) Confirm (within 600 seconds) — otherwise auto-rollback
curl -X POST https://aifw.local/api/v1/config/commit-confirm/confirm \
-H "Authorization: Bearer $TOKEN"
Max XML size is 10 MiB (real OPNsense configs are 50–500 KiB).
Versioned config history
Every save — manual, scheduled, OPNsense import, S3 restore — produces a numbered version. The history endpoints expose them for inspection, diffing, and selective restore.
curl https://aifw.local/api/v1/config/history \
-H "Authorization: Bearer $TOKEN"
# Diff two versions
curl "https://aifw.local/api/v1/config/diff?from=42&to=43" \
-H "Authorization: Bearer $TOKEN"
# Preview what restoring a version would change
curl "https://aifw.local/api/v1/config/restore-preview?version=42" \
-H "Authorization: Bearer $TOKEN"
# Apply
curl -X POST https://aifw.local/api/v1/config/restore \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"version": 42}'
Retention is configurable via GET / PUT /api/v1/config/retention — cap by count, by age, or both.
Commit confirm
Editing remotely and worried about locking yourself out? Wrap the change in commit-confirm. AiFw applies the new config, starts a timer, and reverts unless you explicitly confirm.
# Arm — default timeout is 300 seconds
curl -X POST https://aifw.local/api/v1/config/commit-confirm \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"timeout_secs": 300, "description": "tightening LAN rules"}'
# Check status
curl https://aifw.local/api/v1/config/commit-confirm/status \
-H "Authorization: Bearer $TOKEN"
# Confirm
curl -X POST https://aifw.local/api/v1/config/commit-confirm/confirm \
-H "Authorization: Bearer $TOKEN"
If the timer expires, the snapshot taken at arm time is restored and the config that lost you access is rolled back — same engine writers, same atomic apply.
API endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/config/export |
Export entire config as JSON |
POST |
/api/v1/config/import |
Import a JSON config |
POST |
/api/v1/config/import-preview |
Dry-run a candidate import |
GET |
/api/v1/config/history |
List config-history versions |
GET |
/api/v1/config/version |
Get one version |
GET |
/api/v1/config/diff |
Diff two versions |
GET |
/api/v1/config/check |
Validate the live config |
POST |
/api/v1/config/save |
Save a manual version |
POST |
/api/v1/config/restore |
Restore a version |
GET |
/api/v1/config/restore-preview |
Preview a restore |
GET / PUT |
/api/v1/config/retention |
Retention policy |
POST |
/api/v1/config/preview-opnsense |
Preview an OPNsense XML import |
POST |
/api/v1/config/import-opnsense |
Apply an OPNsense XML import |
POST |
/api/v1/config/commit-confirm |
Arm commit-confirm |
GET |
/api/v1/config/commit-confirm/status |
Status of pending confirm |
POST |
/api/v1/config/commit-confirm/confirm |
Accept the pending change |
GET / PUT |
/api/v1/backup/s3/config |
S3 backup destination config |
POST |
/api/v1/backup/s3/test |
Test S3 credentials + bucket |
GET |
/api/v1/backup/s3/list |
List S3-stored snapshots |
POST |
/api/v1/backup/s3/import |
Restore a snapshot from S3 |
See also
- Comparison with pfSense / OPNsense → (OPNsense migration row)
- Auth & RBAC → (
backup:read/backup:writeperms) - Features overview →
- Source:
aifw-api/src/backup.rs - Source:
aifw-api/src/opnsense/ - Source:
aifw-api/src/backup_s3.rs