Why Bother Backing Up the UniFi Gateway?

I run a UniFi Cloud Gateway Max as the core of my home network. It handles routing, VLANs, firewall rules, and a bunch of other configuration that took a long time to get right. UniFi does offer cloud backup via a Ubiquiti account, but I prefer keeping a local copy on my QNAP NAS — alongside my Home Assistant backups — so everything is in one place and accessible without an internet dependency.

The goal: a weekly automated backup of the CGMax, downloaded via the UniFi OS API and stored in the HASS-Backups share on my QNAP under a unifi subfolder.

Finding the Right API Endpoint

The UniFi OS API is not officially documented for end users, so the first step was figuring out which endpoint actually delivers the backup file. I tried several common paths — /proxy/network/api/s/default/cmd/backup, /api/system/backup, /api/v2/system/backup — all returned 404.

The reliable way to find the real endpoint is to open the UniFi web UI at https://192.168.1.1, open browser DevTools (F12) → Network tab, navigate to System → Backup, and click the download button. The Network tab immediately reveals the request:

GET https://192.168.1.1/api/backup/download

Status 200, Content-Type: application/octet-stream, with a Filename response header containing the full filename — for example unifi_os_backup_1782500441115_7c0a209a.unifi. Note the .unifi extension, not .unf as older documentation suggests.

Authentication works via a session cookie and an X-Csrf-Token header. The CSRF token is returned as a response header on the login call — specifically X-Csrf-Token — and must be extracted from there, not from the JSON body.

The Backup Script

The script lives on the QNAP at /share/HASS-Backups/unifi/unifi_backup.sh. It logs in, grabs the CSRF token from the response header, downloads the backup, validates the file size, rotates old backups, and logs everything.


#!/bin/bash
UNIFI_HOST="192.168.1.1"
UNIFI_USER="your_local_admin"
UNIFI_PASS="your_password"
BACKUP_DIR="/share/HASS-Backups/unifi"
KEEP_DAYS=30
LOG_FILE="/share/HASS-Backups/unifi/unifi_backup.log"

TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
COOKIE_JAR=$(mktemp /tmp/unifi_cookie_XXXXXX)
FILENAME="unifi_backup_${TIMESTAMP}.unifi"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
cleanup() { rm -f "$COOKIE_JAR"; }
trap cleanup EXIT

mkdir -p "$BACKUP_DIR"
log "=== UniFi backup started ==="

LOGIN=$(curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
    -X POST "https://${UNIFI_HOST}/api/auth/login" \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"${UNIFI_USER}\",\"password\":\"${UNIFI_PASS}\"}" \
    -D -)

CSRF_TOKEN=$(echo "$LOGIN" | grep -i "^X-Csrf-Token:" | awk '{print $2}' | tr -d '\r')

if [ -z "$CSRF_TOKEN" ]; then
    log "ERROR: Login failed - no CSRF token received"
    exit 1
fi

log "Login successful. CSRF: ${CSRF_TOKEN:0:8}..."

curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
    -H "X-Csrf-Token: $CSRF_TOKEN" \
    -o "${BACKUP_DIR}/${FILENAME}" \
    "https://${UNIFI_HOST}/api/backup/download"

FILESIZE=$(stat -c%s "${BACKUP_DIR}/${FILENAME}" 2>/dev/null || echo 0)
if [ "$FILESIZE" -lt 10240 ]; then
    log "ERROR: File too small (${FILESIZE} bytes) - backup likely failed"
    rm -f "${BACKUP_DIR}/${FILENAME}"
    exit 1
fi

log "Saved: ${FILENAME} ($(du -h "${BACKUP_DIR}/${FILENAME}" | cut -f1))"

find "$BACKUP_DIR" -name "unifi_backup_*.unifi" -mtime +${KEEP_DAYS} -exec rm -f {} \;
log "Old backups rotated."

curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
    -X POST "https://${UNIFI_HOST}/api/auth/logout" \
    -H "X-Csrf-Token: $CSRF_TOKEN" > /dev/null 2>&1

log "=== Done ==="

A few things worth noting:

  • The script uses a local admin account, not a Ubiquiti cloud SSO account. If you only have a cloud account, create a local admin first under Settings → Admins & Users.
  • The CSRF token must be extracted using grep -i "^X-Csrf-Token:" — matching the exact header line start — because the response also contains an Access-Control-Expose-Headers line that mentions the token name and would otherwise confuse a broader grep.
  • File size validation catches cases where the endpoint returns a JSON error instead of a binary file, which would otherwise silently save a 50-byte error response with a .unifi extension.

Deploying on the QNAP

From an SSH session on the QNAP, the setup is straightforward.

Create the folder and drop the script in place:


mkdir -p /share/HASS-Backups/unifi
nano /share/HASS-Backups/unifi/unifi_backup.sh
# paste the script, update credentials, save
chmod +x /share/HASS-Backups/unifi/unifi_backup.sh

Test it manually before scheduling:


bash /share/HASS-Backups/unifi/unifi_backup.sh
cat /share/HASS-Backups/unifi/unifi_backup.log
ls -lh /share/HASS-Backups/unifi/

A successful run looks like this:


[2026-06-26 21:02:30] === UniFi backup started ===
[2026-06-26 21:02:30] Login successful. CSRF: f6e0a563...
[2026-06-26 21:02:37] Saved: unifi_backup_20260626_210230.unifi (260K)
[2026-06-26 21:02:37] Old backups rotated.
[2026-06-26 21:02:37] === Done ===

Scheduling via Cron

QNAP’s crontab -e is not suid and cannot be used directly. The crontab file lives at /etc/config/crontab and requires sudo to write. Add the weekly job like this:


sudo sh -c 'echo "0 4 * * 0 bash /share/HASS-Backups/unifi/unifi_backup.sh" >> /etc/config/crontab'
sudo crontab /etc/config/crontab && sudo /etc/init.d/crond.sh restart

Verify the entry landed:


crontab -l

The schedule 0 4 * * 0 runs every Sunday at 04:00. Since the QNAP is set to CET, the backup runs at 4am local time — well outside any active hours.

The result is a clean weekly backup pipeline: the QNAP authenticates to the CGMax, pulls the .unifi backup file directly over the local network, saves it timestamped to /share/HASS-Backups/unifi/, and automatically removes files older than 30 days. No cloud dependency, no manual steps.

Privacy Preference Center