unmask

docs

JA4 acquisition, LB / CDN setup, supported distros, FAQ.

Backup, restore & disaster recovery

An operator runbook for backing up an unmask install and restoring it on a new host. The paths below are the defaults — check your /etc/unmask/config.yml for the actual values.

What to back up (and why)

Item Path (default) Why it matters
config.yml /etc/unmask/config.yml Holds bv_secret + captcha_secret_base and the DB connection. Losing or changing bv_secret invalidates every issued _bv cookie, so all current visitors are re-challenged at once (no outage, but a challenge spike). DB credentials live here too.
database SQLite: /var/lib/unmask/unmask.sqlite (+ -wal / -shm)
MariaDB: the unmask_* schema
Admin users, bans, events (= stats), hourly aggregates.
ban file nginx.ban_file in config nginx reads it directly to enforce file-based bans; not reconstructable from the DB alone if it was edited out-of-band.
You do not need to back these up — they are regenerated automatically: /var/lib/unmask/nginx/*.inc (unmask render-nginx rebuilds them), community-bans-*.map (re-pulled from the feed), and the placed ngx_http_unmask_module.so (re-placed on reinstall / nginx start).

Backup

# 1. config + ban file (cheap โ€” do this often)
install -d -m 0700 /backup/unmask
cp -a /etc/unmask/config.yml /backup/unmask/
cp -a "$(awk '/ban_file:/{print $2}' /etc/unmask/config.yml)" /backup/unmask/ 2>/dev/null || true

# 2a. SQLite โ€” consistent snapshot WITHOUT stopping the daemon
sqlite3 /var/lib/unmask/unmask.sqlite ".backup '/backup/unmask/unmask.sqlite'"

# 2b. MariaDB
mysqldump --single-transaction --routines unmask_<db> > /backup/unmask/unmask.sql

--single-transaction (InnoDB) gives a consistent dump without locking. The SQLite .backup command is safe against a live writer — it copies pages under a read lock, whereas a plain cp of a live SQLite file can capture a torn WAL.

Restore

  1. Install the same unmask version that produced the backup (unmask version).
  2. Restore config.yml first — this preserves bv_secret, so already-issued _bv cookies keep validating and visitors are not mass-re-challenged.
  3. Restore the database:
    • SQLite: stop the daemon, copy the file back, start.
    • MariaDB: mysql unmask_<db> < /backup/unmask/unmask.sql.
  4. Restore the ban file to its configured path.
  5. unmask render-nginx && nginx -t && systemctl reload nginx (SysVinit: service nginx reload).
  6. Start / restart the admin service; confirm with unmask doctor and curl -sf http://127.0.0.1:9477/unmask/healthz.

Switching the database driver (SQLite ⇄ MariaDB)

unmask ships no built-in cross-driver data migration. Two options:

Clean switch (recommended when stats history is not precious)

Point db.driver (+ the MariaDB connection) in config.yml at the new driver, run unmask migrate to create the schema there, and restart. The new database starts empty — historical events / stats are left behind, and admin users + bans must be re-created (or imported manually). Easiest done early, before stats accumulate. The admin reconfigure wizard (/admin/setup/ on a configured install) does the same with a UI.

Preserve data (manual)

Dump the old driver, hand-translate the SQL dialect differences (SQLite ↔ MariaDB types / quoting), and load into the schema created by unmask migrate. This is version-specific and unsupported as a one-command path; verify row counts afterward.

Upgrades & verifying

unmask migrate (run by the daemon on start, or manually) is idempotent and re-runnable, which is what makes it safe on MariaDB where DDL is non-transactional (each ALTER / CREATE auto-commits and cannot roll back). Every column / table migration is check-then-applyhasColumn / hasTable is consulted before each ALTER, and the baseline is CREATE TABLE IF NOT EXISTS. A migration interrupted part-way is recovered by simply re-running unmask migrate: the already-applied steps are detected and skipped.

Note (MariaDB only). The one-time unmask_cookie_minute schema-v2 copy that runs on the first start after an upgrade is not transaction-wrapped. If it is interrupted mid-copy and then re-run, it can double-count some cookie-minute stats rows — inflated stats only, with no data loss and no outage. Letting that first post-upgrade start finish uninterrupted avoids it. SQLite is unaffected (its migrations run in a single transaction and roll back atomically).
Verify
unmask doctor          # DB ping + schema + config checks
unmask version         # confirm the restored binary version