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/MySQL: the database named in db.mariadb.database (unmask by default; its tables are unmask_*)
Admin users, bans, events (= stats), hourly aggregates.
uploaded logos /var/lib/unmask/branding/ The challenge-page logos uploaded in Settings (default and per site). Without them a restored install shows no logo until they are uploaded again.
You do not need to back these up — they are regenerated automatically: /var/lib/unmask/nginx/*.inc (unmask render-nginx rebuilds them), the ban file /var/lib/unmask/nginx/banned.txt (rewritten from the bans in the database when the daemon starts), community-bans-*.map (re-pulled from the feed), and the placed ngx_http_unmask_module.so (re-placed on reinstall / nginx start).
Container installs: everything above lives inside the volumes — /etc/unmask (config + secrets), /var/lib/unmask (database) and, with automatic HTTPS, /var/cache/nginx/unmask-acme (the Let's Encrypt account and certificates). Back those volumes up and the table is covered — with the container stopped, since a live SQLite file can be copied torn (or take the SQLite snapshot below from inside it first); run the CLI steps as docker compose exec unmask unmask …. Restoring is restoring the volumes and docker compose up -d — there is no host nginx to render or reload (nginx inside the container picks up the rendered config on its own).

Backup

From 0.1.53 one command takes it all: unmask backup writes unmask-backup-<yyyymmdd-hhmmss>.tar.gz (UTC) holding config.yml, the logos uploaded on the Page design tab and, on SQLite, a consistent copy of the database, taken with VACUUM INTO while the daemon keeps running. On SQLite it lands next to the database, otherwise in the current directory, unless -out DIR names another one. On MariaDB/MySQL the archive holds the config and the logos; dump the database with mysqldump (2b below). Take one before every package upgrade: the schema only moves forward, so going back to an older version needs the database from before the upgrade.

install -d -m 0700 /backup/unmask
unmask backup -out /backup/unmask

By hand, which is also the way on releases before 0.1.53:

# 1. config + uploaded logos (cheap — do this often)
install -d -m 0700 /backup/unmask
cp -a /etc/unmask/config.yml /backup/unmask/
cp -a /var/lib/unmask/branding /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/MySQL
mysqldump --single-transaction unmask > /backup/unmask/unmask.sql   # the db.mariadb.database name

--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. It works with any sqlite3 version: from 0.1.52 the database carries indexes only SQLite 3.38 or newer can read, so an older sqlite3 answers a query with “malformed database schema” (the file is sound), while .backup copies the pages without reading them.

Restore

An unmask backup archive unpacks into the same three parts (mkdir /tmp/unmask-restore && tar -xzf unmask-backup-….tar.gz -C /tmp/unmask-restore); put them back as below.

  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, remove any leftover unmask.sqlite-wal / -shm, copy the file back, chown unmask:unmask /var/lib/unmask/unmask.sqlite (the daemon runs as unmask and cannot write a root-owned file), start.
    • MariaDB/MySQL: mysql unmask < /backup/unmask/unmask.sql.
  4. Put the uploaded logos back in /var/lib/unmask/branding/.
  5. unmask render-nginx && nginx -t && systemctl reload nginx (SysVinit: service nginx reload; OpenRC: rc-service nginx reload).
  6. Start / restart the unmask service; confirm with unmask doctor and curl -sf http://127.0.0.1:9477/unmask/healthz.

Switching the database driver (SQLite ⇄ MariaDB/MySQL)

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/MySQL 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 (/unmask/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/MySQL 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/MySQL where DDL is non-transactional (each ALTER / CREATE auto-commits and cannot roll back). Schema changes are numbered migrations, each recorded in schema_migrations once applied and skipped from then on; the base schema is CREATE TABLE IF NOT EXISTS with check-then-apply steps. After an interruption, re-running unmask migrate continues from the first migration not yet recorded.

Verify
unmask doctor          # DB ping + schema + config checks
unmask version         # confirm the restored binary version