
Automate pfSense and OPNsense Config Backups Off the Box
Before you run this
This guide sets up a read-only pull of the firewall's config.xml to a separate Linux host, scheduled nightly with cron, so you have off-box, dated copies of every configuration. The backup script itself never writes to the firewall — it only copies one file off it over SSH.
There are two distinct pieces, and only one of them changes the firewall:
- On the firewall: you enable the SSH server and add a public key to an admin user. Enabling SSH and opening it to a management subnet is a config change on a live security appliance, so treat it accordingly.
- On the collector (a Linux host): the script and cron job run as an unprivileged user. No root needed there. It only needs write access to its own backup directory.
Because you are touching the firewall's admin access:
- Keep a console/out-of-band session open (physical serial/VGA console or your hypervisor console) while you change SSH settings. A wrong Admin Access rule can lock you out of management.
- Back up the running config first from the GUI: on pfSense, Diagnostics → Backup & Restore; on OPNsense, System → Configuration → Backups. Download that file before you change anything.
- Do it in a maintenance window. If you lock yourself out, the rollback path is the console menu (option to reset to factory / restore) or restoring the config file you just downloaded through the GUI.
- Test on a lab firewall or a single non-production unit first. Read the script, confirm the paths and users match your box, and run it by hand once before you schedule it.
One more thing that matters more here than usual: config.xml contains secrets — password hashes, VPN pre-shared keys, certificate private keys, RADIUS secrets. The copies you pull are as sensitive as the firewall itself. Lock down the backup directory and treat those files as crown jewels.
I assume: pfSense CE 2.7.x / pfSense Plus 24.x and/or OPNsense 24.x, and a Debian/Ubuntu collector host with OpenSSH client and cron. Paths and menus differ on older releases.
Step 1 — Enable SSH on the firewall
pfSense: System → Advanced → Admin Access. Enable Secure Shell Server. I strongly recommend enabling "Require public key authentication" (labelled SSHd Key Only) so passwords are not accepted. Then go to System → User Manager, edit the admin user, and paste your collector's public key into Authorized SSH Keys.
OPNsense: System → Settings → Administration, under Secure Shell tick Enable Secure Shell. If you intend to log in as root, tick Permit root user login; otherwise create a dedicated user. Then System → Access → Users, edit the user, and paste the public key into Authorized keys.
Restrict SSH to your management subnet with a firewall rule on the relevant interface — don't leave it open to everything. Check the vendor admin guide (Netgate docs for pfSense, the OPNsense docs) if the exact wording of a checkbox differs on your build; I won't guess at labels that move between versions.
Step 2 — Make a key on the collector
Run this as the unprivileged user that will own the backups (I'll call it backup):
# ed25519 key, no passphrase so cron can run unattended
ssh-keygen -t ed25519 -f /home/backup/.ssh/fw_backup -N "" -C "fw-config-backup"
cat /home/backup/.ssh/fw_backup.pub # paste this into the firewall in Step 1
A passphrase-less key is the standard trade-off for unattended jobs. Contain the risk by keeping the private key chmod 600, owned by backup, and by restricting where the firewall accepts it from.
Test the pull by hand before automating anything:
# pfSense: user is usually 'admin', config lives at /cf/conf/config.xml
scp -i /home/backup/.ssh/fw_backup admin@192.0.2.1:/cf/conf/config.xml /tmp/test.xml
# OPNsense: user 'root' (or your dedicated user), config at /conf/config.xml
scp -i /home/backup/.ssh/fw_backup root@192.0.2.2:/conf/config.xml /tmp/test-opn.xml
Replace 192.0.2.1 / 192.0.2.2 with your firewall addresses and the users with yours. If those two commands don't produce a real XML file, fix that before scheduling — don't debug inside cron.
Step 3 — The backup script
Save this as /home/backup/bin/fw-config-backup.sh and chmod 700 it. Edit the FIREWALLS list and paths to match your environment.
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/firewalls" # where dated copies land on this host
KEY="/home/backup/.ssh/fw_backup" # the private key from Step 2
RETENTION_DAYS=30 # delete copies older than this
# name -> user@host:/remote/path/to/config.xml (edit to match your kit)
declare -A FIREWALLS=(
["pfsense-hq"]="admin@192.0.2.1:/cf/conf/config.xml"
["opnsense-dc"]="root@192.0.2.2:/conf/config.xml"
)
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR" # secrets live here — keep it tight
STAMP="$(date +%Y%m%d-%H%M%S)"
rc=0
for name in "${!FIREWALLS[@]}"; do
target="$BACKUP_DIR/${name}-${STAMP}.xml"
if scp -q -i "$KEY" -o StrictHostKeyChecking=accept-new \
"${FIREWALLS[$name]}" "$target"; then
chmod 600 "$target"
# sanity check: non-empty and looks like a firewall config
if [[ -s "$target" ]] && grep -qE '<(pfsense|opnsense)>' "$target"; then
echo "OK: $name -> $target"
else
echo "SUSPECT (empty or not a config): $name" >&2
rc=1
fi
else
echo "FAILED to fetch: $name" >&2
rc=1
fi
done
# Rotation: only prunes this directory's *.xml files by age
find "$BACKUP_DIR" -maxdepth 1 -name '*.xml' -type f -mtime +"$RETENTION_DAYS" -delete
exit "$rc"
StrictHostKeyChecking=accept-new pins the host key on first contact and refuses it if it ever changes — safer than turning host-key checking off. The grep guard means a truncated or wrong file is flagged, not silently kept.
Step 4 — Schedule it
Add a cron entry for the backup user (crontab -e -u backup, or crontab -e while logged in as backup):
# Pull firewall configs at 02:30 daily; log output
30 2 * * * /home/backup/bin/fw-config-backup.sh >> /var/log/fw-backup.log 2>&1
If you prefer systemd timers over cron, that's the other standard option; the script is identical.
Verify it worked
Run the script once by hand and check the output line reads OK: for each firewall, then inspect the files:
ls -l /var/backups/firewalls/
xmllint --noout /var/backups/firewalls/pfsense-hq-*.xml && echo "well-formed XML"
xmllint ships in the libxml2-utils package on Debian/Ubuntu. A well-formed file that contains a <pfsense> or <opnsense> root element is a real, restorable config. After the next scheduled run, confirm a new dated file appeared and that /var/log/fw-backup.log shows the run.
Restoring, and undoing this setup
To restore a config onto a firewall, use the GUI, not SCP: pfSense Diagnostics → Backup & Restore → Restore configuration, or OPNsense System → Configuration → Backups → Restore. Upload the dated .xml you pulled. The firewall applies it and usually reboots. Restoring is a full config replacement — do it in a maintenance window with the console open.
To back this automation out: remove the cron line, delete the script and key, remove the public key from the firewall user, and — if you enabled SSH only for this — disable the Secure Shell Server again in the same menu from Step 1. Finally, if you're decommissioning, securely delete the pulled configs (they hold live secrets); a plain rm of the backup directory is enough once you're certain you no longer need them.
Other approaches worth knowing by name
If you want a hands-off cloud target instead of your own collector: pfSense ships the AutoConfigBackup package (encrypted backups to Netgate's service), and OPNsense has built-in backup to Google Drive or Nextcloud under System → Configuration → Backups. There's also an authenticated HTTPS/API pull on both platforms. I've shown the SSH pull because it's the most portable and the least likely to break when the GUI changes between releases — but any of those are legitimate, and the vendor docs cover them.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Automate MySQL/MariaDB Backups With Rotation and a Restore Test
Two scripts here. The first ( mysql-backup.sh ) makes a gzipped mysqldump of every database on the server, drops the file in /var/backups/mysql , and deletes any dump older than a retention window. The second ( mysql-restore-test.sh ) proves a backup is usable by loading the newest dump for one database into a throwaway database, counting the tables, and dropping that throwaway again.
Schedule and Verify PostgreSQL Backups with pg_dump and Retention
This guide sets up a bash script that runs pg_dump (and pg_dumpall for cluster-wide roles) on a schedule, writes each backup to a directory with a timestamped filename, and then deletes any backup older than a retention window. The purpose is a hands-off nightly logical backup you can restore from.
Automate Daily Encrypted Backups With rsync and cron
This guide builds a small shell script that runs once a night from cron. Each run it makes a compressed tar archive of one source directory, encrypts that archive to a GPG public key , deletes the plaintext copy, then pushes the encrypted file to a remote server over SSH with rsync . The result is a backup that is encrypted at rest (GPG) and in transit (SSH). Only someone holding the matching GPG private key can read it — so keep that private key off the backup box.
Scheduled Database and File Backups to S3-Compatible Storage with rclone
This sets up an unattended nightly job that dumps your MySQL/MariaDB databases and tars up a couple of directories, uploads both to an S3-compatible bucket with rclone , and then deletes backups in that bucket older than a retention window. Its purpose is a hands-off off-site copy that prunes itself so the bucket doesn't grow forever.




