unmask

docs

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

FAQ

Which topologies expose JA4? Is it unavailable with forward-auth?
JA4 availability depends on where TLS is terminated. nginx + unmask-plugin-nginx (native mode) gets it; without the plugin (forward-auth mode) it does not; with an upstream LB / CDN you get it via header forwarding. See JA4 acquisition for a per-topology diagram.
What happens in forward-auth mode when the daemon goes down? Can it fail closed?
forward-auth fails open completely by default when the daemon stops. Protected paths pass through the gate (auth_request), and a mid-challenge visitor is 302'd back to / and flows through to your backend (the same outcome as native mode, which replays the original request). The site stays up.

To fail closed instead — return 503 so an upstream LB drains this node via its health check, or pass the original request to your own backend — define @unmask_admin_down before the include (nginx keeps the first definition of a named location — placed after the include it is silently ignored):
# (a) fail closed so an upstream LB drains this node:
location @unmask_admin_down { add_header Retry-After "10" always; return 503; }
# (b) pass the original request to your own backend:
location @unmask_admin_down {
    proxy_pass http://your-upstream;          # static site: try_files $uri $uri/index.html =404;
    proxy_set_header Host $host;
}
include /etc/unmask/forward-auth/server.inc;
Some events have an empty JA4 verdict ("-"). Is that a problem?
It's fine. It happens on requests where the upstream (nginx native module / GCP LB / etc.) couldn't obtain a JA4 (TLS resumption / certain unusual handshakes). It is not caused by unmask.
The stealth column is showing red. Should I worry?
"All JS detections (flags) cleared + classified as a bot by JA4" is a sign that a modern headless bot is hitting you. Blocked verdicts are still forced into a challenge, so as long as the "JA4 bot/suspect × CAPTCHA passed" dashboard card stays at zero, nothing actually got through.
Which visitors are challenge targets by default?
By default every visitor that is not a bypass IP / bypass path / search bot is a challenge target (the no-match KnownBrowserAction / UnknownUAAction, when unset, both resolve to pow_only). PoW is automatic and needs no interaction, so the load on ordinary visitors is minimal. Search bots still pass safely because the UA preset and the official IP range bypass list are applied as OR, so no SEO accidents. For a lighter posture ("normal browsers pass, only curl / scrapers are challenged"), set KnownBrowserAction to pass under settings → Operating mode tab.
I'm behind a CDN / reverse proxy. Will decisions use the real client IP?
IP-based decisions (BAN / rate limit / country filter / bypass IPs) use the client IP that nginx sees. Behind a CDN / proxy / LB that is the upstream's IP by default, so configure stock nginx set_real_ip_from (the trusted upstream ranges) and real_ip_header (CF-Connecting-IP for Cloudflare, X-Forwarded-For in general) to restore the real IP from the forwarded header. This is plain nginx real_ip, not unmask-specific.

Without it, every request looks like the same upstream IP and rate limit / country filtering won't behave as intended. JA4 availability is a separate matter, decided by where TLS is terminated (a CDN that terminates TLS needs header forwarding). See JA4 acquisition and load-balancer setup.
A legitimate user got challenged / blocked by mistake. What do I do?
First, the default action is a PoW / CAPTCHA, not an outright block, so a real user passes by solving the challenge (PoW is automatic, CAPTCHA is once). To reliably let a specific client or path through, loosen it narrowly:
・trusted IPs (internal / monitoring / LB egress) → settings → Bypass IPs
・specific paths (public APIs etc.) → settings → Bypass paths
・a JA4 misclassified as a bot → settings → JA4 verdict tab, set that verdict's action to observe / allow
・too strict overall → set KnownBrowserAction to pass under the Operating mode tab (normal browsers pass, only curl / scrapers are challenged)
The hunt screen shows the JA4 / fingerprint / reason for each request — check the cause first, then loosen only what's affected.
Is unmask a WAF? Does it block vulnerability scans and attacks?
unmask is a bot-management gateway, not a WAF. A WAF inspects the contents of a request (SQLi / XSS and other payloads); unmask decides who is allowed to reach your app — by JA4 / PoW / CAPTCHA / behavioral signals — and turns automated clients away at the edge.

The security upside is a real side effect: most vulnerability scanners raking over wp-login.php, .env, and known-CVE paths, plus brute-force and credential stuffing, are non-browser clients that don't run JS. They hit the PoW / CAPTCHA gate at the door, so the number of attack attempts drops (lowering breach and data-leak risk). Even as AI makes finding flaws and launching attacks cheaper and faster, unmask keeps a cost on automated, high-volume access.

That said, unmask does not patch the vulnerabilities themselves and does not inspect payloads. A one-off attack from a real browser that solved the challenge, and applying patches for known CVEs, belong to other layers. unmask is one layer of defense-in-depth, meant to run alongside your WAF and patch process.
What is the PoW algorithm?
SHA-256 hashcash (challenge.js's built-in pure-JS SHA-256 implementation; it searches for a nonce whose hash starts with N leading-zero bits). Difficulty (leading-zero bits) is configurable in settings → Challenge tab (default 18 bits, typically ~1 s).
Migrating from SQLite to MariaDB — or the reverse (changing the DB connection)
Switch via the admin reconfigure wizard or by editing config.yml directly. The steps are the same in either direction (SQLite → MariaDB or MariaDB → SQLite). Either way, past events / bans / users are not carried over to the target DB (your original DB is left untouched; if the target already holds unmask data it is adopted, if empty a fresh schema is created).

Option A — reconfigure wizard (easiest): log in as a superadmin and open /admin/setup/ — on a configured install it becomes the reconfigure wizard. At the DB step pick the target driver (SQLite / MariaDB) and enter its details (MariaDB: host/DB/user; SQLite: a file path) → create the first admin if the target is empty (or “skip” to adopt an existing one) → review → apply. Switching the DB rebinds the background workers, so the daemon restarts itself automatically (no manual step).

Option B — CLI / manual: (1) stop admin (2) set config.yml db.driver to the target (mariadb / sqlite) with its connection info (MariaDB: create the DB + user first; SQLite: set sqlite_path) (3) unmask doctor to verify the connection and flag any missing tables (4) unmask migrate to bootstrap the schema (5) unmask user create admin -role superadmin (6) start admin.
What do the reasons in the Stats-tab "Forced CAPTCHA (reason breakdown)" card mean?
none=normal PoW (not forced) / ja4_bot=JA4 verdict bot / honeypot=honeypot trip / banned=BAN list hit (incl. community / manual / honeypot; see the Community Bans / BAN pages for the source breakdown) / protected=protected-path mode / rate_limit=rate-limit exceeded / test=debug path. If one reason dominates, you can act on it directly.
"(SSE connection error)" keeps appearing
Most often an LB / proxy in front cuts the idle connection (e.g. GCP LB's backend timeout, 30 s by default). A heartbeat is implemented at 20 s. The "(reconnecting)" message means EventSource is auto-retrying. "Connection error" is only shown when it's fully closed.
Does unmask expose Prometheus metrics?
Yes — the admin daemon serves a Prometheus-format endpoint at /unmask/metrics: challenge volume by phase / verdict, the behavioral-score histogram, and DB latency. It is loopback-only by default; add your scraper's IP / CIDR to metrics_allow_from (settings → Network) to scrape it remotely. There is also an unauthenticated /unmask/healthz (returns ok) for liveness probes. See Monitoring.
What if I leave admin_allow_from empty?
It's treated as "allow all" (all IPs permitted). Defense relies on authentication alone. If non-empty, the IP restriction applies at both the handler layer and the nginx render.
I switched to a third-party CAPTCHA and got locked out of the admin
When the unmask itself protected-path preset is on, /unmask/admin/ is covered by a CAPTCHA. Turnstile, hCaptcha and reCAPTCHA all require registering the domains they run on, so if the hostname you reach the admin by isn't in that CAPTCHA's allowed list, the CAPTCHA button won't render and you can't get in (common when you open the admin by a bare server hostname, or on a multi-host setup). To recover, set challenge.default.captcha.provider to builtin in /etc/unmask/config.yml and restart unmask — the built-in CAPTCHA works on every host, so you can log back in. On multi-host setups, built-in is the safe default.
I want to install / update the IP-geolocation database (mmdb)
The easiest way is sudo unmask install-ipgeo — it auto-downloads DB-IP Lite (CC BY 4.0, no signup) and places it under /var/lib/unmask/ipgeo/. Add it to cron for monthly refreshes. Or fetch it manually from the "📥 dl / refresh" button under settings → Country filter tab. The mmdb hot-reloads, so no admin restart is needed.
I forgot to create a user after install
sudo unmask user create <username> -role superadmin -password <pw>. Or use the wizard at /admin/setup/.
I forgot my password
sudo unmask user reset-password <username> -password <new>.
On CentOS 6 / 7, feed sync fails with certificate signed by unknown authority
An EOL distro's CA store can be too old to verify unmask's feed / auto-update over TLS, lacking Let's Encrypt's current root — not an unmask bug; unmask defers to the system trust store by design. Run sudo yum update ca-certificates, or add ISRG Root X1 / X2 to the trust store, then restart unmask. The bundled snapshot keeps rescuing search bots until sync recovers.