install
Pick how unmask sits in front of your site — nginx module, gateway container or forward-auth — and your OS, to get the exact yum / apt / apk / docker compose commands and config snippet.
0 Choose where unmask sits
Click a card; the steps below follow it.
nginx native module
The dynamic module goes into the nginx you already run; decisions happen inside the nginx process, which makes it the fastest shape.
- ✓ Once a client has passed the challenge, it runs at the same speed as bare nginx (the server just verifies a short-lived pass cookie)
- ✓ ~0.05 ms per request (cookie verification within the same process. No subrequest / no IPC)
- ✓ rate_limit / honeypot paths are handled entirely within nginx
- △ nginx only (anything else: the gateway container)
gateway container
A container bundle — nginx with the module built in, plus the unmask daemon — placed in front of your existing HTTP server; nothing behind it changes.
- ✓ In front of any HTTP server (Apache, Caddy, Node, Rails, IIS, another nginx)
- ✓ Sees every visitor's TLS fingerprint (JA4) (the container terminates TLS, with the native module inside)
- ✓ HTTPS handled for you (Let's Encrypt automatically, or paste the certificate you bought into the admin)
- △ Needs Docker (two containers from one compose file: admin + nginx with the module)
forward-auth mode
To wire into the nginx / Apache you already run, pick this — nothing sits in front; stock nginx (auth_request) / Apache (mod_lua). ~0.5–2 ms per request via subrequest, and JA4 only when a front LB forwards it (see 5.3).
1 Choose your environment
Every option above is CI-verified. Derivative distros (openSUSE / Mint / Pop!_OS / etc.) and the full matrix live on the supported distro list.
2 Install the unmask core ?repo method: install unmask-release once first to set up the repo config + GPG public key → from then on you can install / upgrade the core with dnf install unmask / apt install unmask. Signature verification is handled automatically by the package manager.
Right after install: admin auto-starts via systemd (127.0.0.1:9477) and writes /etc/unmask/.setup-token. The setup wizard launches in section 4.
Upgrade: sudo dnf upgrade unmask && sudo systemctl restart unmask (on deb, sudo apt upgrade unmask).
sudo dnf install -y https://unmask.sh/dl/rpm/unmask-release-latest.noarch.rpm sudo dnf install -y unmask
3 Install unmask-plugin-nginx (native plugin) ?Prerequisite: nginx must be installed (auto-pulled in step 4 via unmask-web-nginx dependencies).
postinstall: places load_module into the distro's conventional dir (/usr/share/nginx/modules/ or /etc/nginx/modules-enabled/) automatically. 14 nginx versions × OpenSSL 1.0 / 1.1 / 3 ABIs are bundled (1.10–1.30 series / glibc) so the matching module is picked at install time. No manual editing.
After an nginx upgrade: ABI mismatch surfaces as nginx -t errors → sudo dnf upgrade unmask-plugin-nginx (or sudo apt install --only-upgrade unmask-plugin-nginx on deb) realigns versions.
sudo dnf install -y unmask-plugin-nginx4 Install unmask-web-nginx (web config snippet) ?postinstall places /etc/unmask/forward-auth/{server,protect}.inc + the upstream symlink, validates with nginx -t, and prints the setup token / URL. It does not touch the running nginx — the wizard URL becomes reachable after the restart you run below.
The JA4 maps include is fully automatic: postinstall places a symlink at /etc/nginx/conf.d/00-unmask.conf → /var/lib/unmask/nginx/http.inc. Since every distro's default include /etc/nginx/conf.d/*.conf; auto-loads it, you do not need to edit the http {} block.
sudo dnf install -y unmask-web-nginx
/unmask/admin/setup/.
- [Recommended] Add one line to an existing vhost — open the setup wizard on the domain you already use. Inside the target
server { }block, add the blue (highlighted) line:server { listen 443 ssl http2; server_name example.com; ssl_certificate ...; include /var/lib/unmask/nginx/server.inc; # ← unmask itself (challenge serving, BAN, admin UI) root /var/www/example; }→sudo nginx -t && sudo systemctl restart nginx(a restart — not reload — is what loads the freshly installed module) → openhttps://<your-domain>/unmask/admin/setup/. - [Direct port] — nothing to change if you can shell into the host itself:
http://127.0.0.1:9477/unmask/admin/setup/is reachable out of the box. From a different host, use the SSH tunnel below. - [SSH tunnel] — a universal fallback that needs no firewall changes.
ssh -L 9477:127.0.0.1:9477 your-server -N &
→ openhttp://localhost:9477/unmask/admin/setup/.
A challenge on the setup URL only means the admin UI (/unmask/admin/) is protected. Your site itself stays unprotected until you add protect.inc in the next step.
The wizard is three screens: setup token (shown at install time, or sudo cat /etc/unmask/.setup-token) → admin account → DB (the SQLite default is enough). It then drops you on the admin home; remaining settings live under /unmask/admin/settings/.
5 Fire the challenge in the server {} you want to protect (ON/OFF switch) ?These highlighted lines are unmask's ON/OFF switch. Add them and bot protection fires; remove them and blocking / challenges stop immediately, with no effect on user traffic. The access-log statistics feed keeps flowing either way, so a safe rollout is step-by-step: monitoring (stats only) → trial (fire on one location) → production (fire site-wide).
What you actually write is 2 lines: include /var/lib/unmask/nginx/server.inc; directly inside server { }, and include /var/lib/unmask/nginx/protect.inc; wherever you want protection. Everything else (rate-limit zones, challenge dispatch, JA4 maps) is rendered by the admin into /var/lib/unmask/nginx/ and auto-loaded via the conf.d symlink placed in section 4.
Rate / burst / target paths live on the Settings → rate-limit tab. To restrict which domain can reach the admin UI, use Settings → Network → admin_allowed_hosts.
In the target server {} block (the .conf of your existing nginx vhost), add the blue (highlighted) lines. Everything else is your existing config — a reverse proxy is shown, but a static site (root + try_files) adds the same lines.
No need to edit existing locations — just add this one block.
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate ...;
include /var/lib/unmask/nginx/server.inc; # ← unmask itself (challenge serving, BAN, admin UI)
include /var/lib/unmask/nginx/protect.inc; # ← outside any location (protects every location)
location / { # ← your existing location stays unedited
proxy_pass http://your-upstream;
}
}
Once you're done editing, apply the config. If you have not restarted nginx since installing the plugin, restart (a reload does not load a new module); afterwards plain reloads are enough:
sudo nginx -t && sudo systemctl restart nginx
1 Get the compose file ?Two containers, one file: admin (the daemon: admin UI, challenge verification, ban list, stats) and nginx (the official nginx image with the unmask module). Images come from unmask.sh/ (served like the packages; mirrored at ghcr.io/unmask-sh/), and a stand-in app is included so the example answers on its own.
The volumes matter: /etc/unmask holds the rendered nginx config and the secrets that sign pass cookies, /var/lib/unmask is the database. Keep them across upgrades.
Upgrade: docker compose pull && docker compose up -d.
Runs on any Linux with Docker Engine and Compose v2 (Docker Desktop on macOS / Windows too) — there is no OS / distro to choose.
mkdir unmask && cd unmask curl -fsSLO https://unmask.sh/dl/docker/docker-compose.yml
2 Start it and pick up the token ?The first start brings :443 up with a self-signed certificate (your browser warns once). The upstream, the hostnames and the real certificate are all set in the admin UI in the next step (Settings → Gateway), so the compose file needs no editing.
The gateway forwards passing requests with Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto, passes WebSocket upgrades, and sets no body-size limit of its own.
Listen 8080, nginx: listen 8080;, then reload (bound to an address the container can reach, so not 127.0.0.1; keep 8080 closed at the firewall). From here until the upstream is set in step 3, visitors see the gateway's notice page for a few minutes. Two-proxy details are in Containers (docs). On a fresh host, skip this paragraph.
docker-compose.yml works as it is. Start it and pick up the setup token from the log:
docker compose up -d docker compose logs admin | grep "setup token"
3 Finish the wizard, then set up the Gateway ?The admin UI sits behind the same protection as the rest of the site, so the first thing you see on the setup URL is a short security check; pass it and the wizard follows. Only /unmask/admin/ is challenged at this point — until an upstream is set the gateway answers with a notice saying so, and once it is, your site is served as before.
Certificates: Settings → Gateway shows what :443 is serving right now (issuer, expiry, whether the name matches). Pick Let's Encrypt and enter a contact address (the hostname must resolve to this host, with :80 reachable; the account is created on the first issuance), or paste a purchased certificate there (certificate, chain, key — the key is stored readable by nobody else and never shown again); a renewal is pasting the new pair.
https://<this host>/unmask/admin/setup/ (accept the self-signed certificate warning once) and finish the wizard — setup token (from docker compose logs admin above) → admin account → DB (the SQLite default is enough). Then:
Settings → Gateway — enter your server as the upstream; three examples (a server on this host / another host / a container in the same compose) sit right under the field. A server on this host is the one you moved to 8080 in step 2 (http://host.docker.internal:8080). The hostnames can stay at "any" or be narrowed to the ones visitors use; switch the certificate from self-signed to Let's Encrypt or a pasted one.
2 Install the unmask core ?repo method: install unmask-release once first to set up the repo config + GPG public key → from then on you can install / upgrade the core with dnf install unmask / apt install unmask. Signature verification is handled automatically by the package manager.
sudo dnf install -y https://unmask.sh/dl/rpm/unmask-release-latest.noarch.rpm sudo dnf install -y unmask
3 Install unmask-web-nginx (web config snippet) ?Prerequisite: nginx must already be installed (if not, dependency resolution installs it automatically). For forward-auth mode the distro's bundled nginx is fine.
postinstall: places /etc/unmask/forward-auth/{server,protect}.inc + the upstream symlink, validates with nginx -t, and prints the setup token / URL. It does not touch the running nginx — the wizard URL becomes reachable after the reload you run below.
sudo dnf install -y unmask-web-nginx
/unmask/admin/setup/.
- [Recommended] Add one line to an existing vhost — open the setup wizard on the domain you already use. Inside the target
server { }block, add the single lineinclude /etc/unmask/forward-auth/server.inc;
→sudo nginx -t && sudo nginx -s reload→ openhttps://<your-domain>/unmask/admin/setup/. - [Direct port] — nothing to change if you can shell into the host itself:
http://127.0.0.1:9477/unmask/admin/setup/is reachable out of the box. From a different host, use the SSH tunnel below. - [SSH tunnel] — a universal fallback that needs no firewall changes.
ssh -L 9477:127.0.0.1:9477 your-server -N &
→ openhttp://localhost:9477/unmask/admin/setup/.
sudo cat /etc/unmask/.setup-token) → admin account → DB (the SQLite default is enough). It then drops you on the admin home.
4 Enable forward-auth in the server {} you want to protect (ON/OFF switch) ?This single block is unmask's ON/OFF switch. Add it and bot protection runs; remove it and blocking / challenges stop immediately.
Dashboard aggregation path: in forward-auth mode, only requests for which the /_unmask/check subrequest was called are counted in the kind/cnt aggregation (the cookie-pass chart). The syslog path used by nginx-native mode (counts every request) is not used here, so requests outside protected locations don't show up in the dashboard. If you want every request counted, choose nginx-native mode.
In the target server {} block (the .conf of your existing nginx vhost), add the blue (highlighted) lines. Everything else is your existing config — a reverse proxy is shown, but a static site (root + try_files) adds the same lines.
No need to edit existing locations — just add this one block.
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate ...;
include /etc/unmask/forward-auth/server.inc; # ← unmask itself (challenge serving, BAN, admin UI)
include /etc/unmask/forward-auth/protect.inc; # ← outside any location (protects every location)
location / { # ← your existing location stays unedited
proxy_pass http://your-upstream;
}
}
Once you're done editing, reload nginx to apply the config:
sudo nginx -t && sudo systemctl reload nginx
2 Install the unmask core ?repo method: install unmask-release once first to set up the repo config + GPG public key → from then on you can install / upgrade the core with dnf install unmask / apt install unmask. Signature verification is handled automatically by the package manager.
sudo dnf install -y https://unmask.sh/dl/rpm/unmask-release-latest.noarch.rpm sudo dnf install -y unmask
3 Install unmask-web-apache (auto-places the snippet) ?Prerequisite: Apache must already be installed (if not, dependency resolution also auto-installs httpd + mod_lua + mod_proxy_http).
postinstall: places /etc/httpd/conf.d/unmask-web.conf + /etc/httpd/unmask.lua, runs apachectl graceful, and prints the setup token / URL. The conf.d snippet sets up the /unmask/* proxy (makes https://<host>/unmask/admin/setup/ reachable from every VirtualHost) but does NOT auto-protect any VirtualHost — you opt in per-VirtualHost by adding the LuaHookAccessChecker line in section 4 (consistent with nginx mode's per-server include protect.inc;).
sudo dnf install -y unmask-web-apache
https://<your-vhost>/unmask/admin/setup/ (with Apache, the conf.d snippet makes /unmask/* reachable from every VirtualHost). The wizard is three screens: setup token (shown at install time, or sudo cat /etc/unmask/.setup-token) → admin account → DB (the SQLite default is enough). It then drops you on the admin home.
4 Enable forward-auth in the VirtualHost you want to protect (ON/OFF switch) ?This single block is unmask's ON/OFF switch. Add it and bot protection runs; remove it and blocking / challenges stop immediately.
Dashboard aggregation path: unlike nginx-native (plugin + unix datagram socket access_log), in Apache mode the kind/cnt aggregation (the cookie-pass chart) is incremented by 1 each time a request reaches admin's /_unmask/check via the forward-auth subrequest. In other words only requests for protected VirtualHosts are aggregated — it's not an "all requests counted" model like nginx-native.
fail-open: /etc/httpd/unmask.lua returns OK when the subrequest to admin fails (the request flows through to the backend / DocumentRoot).
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile ...
LuaHookAccessChecker /etc/httpd/unmask.lua handle_request
ProxyPass / http://your-upstream/
ProxyPassReverse / http://your-upstream/
</VirtualHost>
sudo apachectl configtest && sudo systemctl reload httpd
5 Verify + troubleshoot
# plain curl — no browser TLS, no JS. It should be served the challenge curl -sk https://example.com/ | grep -oE 'unmask|challenge' | head -1 # → if "unmask" appears, the challenge HTML is being served
The Stats tab → the "Challenge funnel" card. Success if serve / load / pow / captcha are increasing.
When TLS is terminated at an LB / CDN, extract the real client's JA4 on the LB and forward it via an X-Client-JA4 header. Per-LB setup (GCP / Cloudflare / AWS) lives in Capturing JA4 from an LB (docs) ↗. Behavior when JA4 is empty: What JA4 does (docs) ↗.
- 403 forbidden appears → add your IP / CIDR to
admin_allowed_ipsin settings → Network tab - JA4 verdict is empty → in native mode, check the
load_moduleconfig; in forward-auth mode, check 5.3 (the LB'sX-Client-JA4header forwarding) - module ABI mismatch on nginx -t → nginx was upgraded but the module didn't follow. Align the versions with
sudo dnf upgrade unmask-plugin-nginx(on deb,apt install --only-upgrade unmask-plugin-nginx) - The challenge HTML doesn't show → check that unmask is listening on
127.0.0.1:9477(systemctl status unmask) - 502 from the gateway container → the upstream (Settings → Gateway) is not reachable from inside the container. A server on the Docker host is
http://host.docker.internal:8080, bound to an address the container can reach (not127.0.0.1). A "no upstream yet" notice means the upstream has not been entered at all - The gateway serves no / the wrong certificate → Settings → Gateway shows what :443 is serving. Automatic HTTPS needs the hostname to resolve to this host and :80 reachable from the internet; a purchased certificate is pasted on the same tab