Skip to main content

Installing Blocklist

Two images: the API, and an optional console. The API answers on 5441, the console on 5443.

Deploying it​

You need Docker with the Compose plugin and an API key of your own choosing (any long random string, for example from openssl rand -base64 32).

Save this as docker-compose.yml:

services:
blocklist:
image: ghcr.io/antelao-io/blocklist:${BLOCKLIST_VERSION:?set BLOCKLIST_VERSION}
restart: unless-stopped
ports:
- "5441:8080"
environment:
Api__Keys__ui: ${BLOCKLIST_API_KEY:?set BLOCKLIST_API_KEY}
BLOCKLIST_SEED_ADMIN_PASS: ${BLOCKLIST_SEED_ADMIN_PASS:-}
BLOCKLIST_SEED_ADMIN_TOTP: ${BLOCKLIST_SEED_ADMIN_TOTP:-}
volumes:
- blocklist-data:/data
- /etc/machine-id:/etc/machine-id:ro

blocklist-ui:
image: ghcr.io/antelao-io/blocklist-ui:${BLOCKLIST_VERSION:?set BLOCKLIST_VERSION}
restart: unless-stopped
profiles: [console]
ports:
- "127.0.0.1:5443:8080"
environment:
BlocklistApi__BaseUrl: http://blocklist:8080
BlocklistApi__ApiKey: ${BLOCKLIST_API_KEY:?set BLOCKLIST_API_KEY}
DataProtection__KeysPath: /keys
volumes:
- blocklist-ui-keys:/keys
depends_on:
blocklist:
condition: service_healthy

volumes:
blocklist-data:
blocklist-ui-keys:

Put the values in a .env file beside it:

BLOCKLIST_VERSION=<the release you run>
BLOCKLIST_API_KEY=<your API key>
BLOCKLIST_SEED_ADMIN_PASS=<the console administrator's password>
BLOCKLIST_SEED_ADMIN_TOTP=<optional: the administrator's authenticator secret>

Then start the API alone, or the API and the console:

sudo docker compose up -d
sudo docker compose --profile console up -d
  • The version is pinned. Both images carry the same release number. Keep a release number in BLOCKLIST_VERSION rather than latest, which moves the moment a new release is published, so that you can always say what is deployed. The releases, and what each one changed, are on Release notes; a registry tool lists the tags, for example skopeo list-tags docker://ghcr.io/antelao-io/blocklist. To upgrade, change the number and run sudo docker compose pull and the up -d command again. Check the images' signatures too: see Verify a download.
  • The API is published on every interface in this example, over plain HTTP. Decide who may reach it before you start it: see Keeping the API key off the network.
  • The console is published on loopback only. It is a sign-in page for people, not an API for machines. Reach it over an SSH tunnel (http://localhost:5443), or put a reverse proxy with HTTPS in front of it before publishing it anywhere else - passkeys work only over HTTPS, and the console keeps a sign-in only over HTTPS or on localhost. Sign in as admin with BLOCKLIST_SEED_ADMIN_PASS, and a code from the authenticator app when BLOCKLIST_SEED_ADMIN_TOTP is set, then add accounts for everyone else on the Users page; see Accounts.
  • Behind a reverse proxy, name it with Ui__TrustedProxies__0 (its address, or its network). Otherwise the console sees every request as coming from the proxy over plain HTTP: every sign-in is throttled and audited under the proxy's address. With none named, a forwarded address is believed from nobody, so a caller cannot name its own. A proxy in front of the API itself is named the same way with Api__TrustedProxies__0. See A proxy that is not trusted.
  • The key the console uses is named ui in the example. If yours has another name, set Api__ConsoleKeyName to it, or the audit log records the console's address for everyone who signs in through it.
  • Keep the blocklist-data volume across an upgrade. It holds the service's database - your own entries and exceptions, the lists' settings and the copies downloaded, the jobs, the issued keys, the accounts and the audit log - and the license. Losing it loses your entries and accounts, drops the installed license, and downloads every list again.
  • /etc/machine-id is mounted because the license is bound to the host, not the container. Without it every recreated container looks like a different machine.
At least one API key is required

Api__Keys__<name> in the API container's environment. Everything except /health needs the X-Api-Key header, and a service with no configured key cannot be reached at all. See API keys.

Outbound access​

The service downloads each public list from its publisher, over HTTPS, on the list's schedule. Let the API container reach the hosts each list's card names in the console: the Source line of every card on the Blocklists page, which GET /api/v1/sources returns as sources for each list too. Only the lists your license covers are shown and downloaded, so the list of hosts follows the license.

Two more things go out:

  • The mail-domain check asks DNS, through the resolvers the container is given, for the domain's mail servers. When DNS does not answer in time or answers with a failure, that check says unknown rather than yes or no; see The mail-domain check.
  • The license check-in, once a day. See Licensing.

A download that cannot reach its host fails as a job, and the list keeps the entries it already had. Nothing that calls the checks goes down because a list could not be refreshed.

Keeping the API key off the network​

The API answers over plain HTTP, and every call carries the key

The API has no TLS of its own. Every call sends the API key in the X-Api-Key header, so on a network you do not control anyone who can see the traffic can read the key and use it.

A stolen key can read and change your entries and your lists, and read the firewall log.

Publish the API no wider than its callers need. The console reaches it over the compose network (http://blocklist:8080), so none of these changes affects the console.

Who calls the APIWhat to do
Only programs on this hostPublish it on loopback: "127.0.0.1:5441:8080"
Machines on a private networkPublish it on this host's private address only, for example "10.0.0.5:5441:8080", and admit only the callers in the host firewall. Docker routes published ports past the host's INPUT rules, so filter them in the DOCKER-USER chain
Machines across the internetPut a reverse proxy with HTTPS in front of it, publish the API on loopback for the proxy to reach, and name the proxy as trusted

With a reverse proxy on the same host, the API side of the compose file becomes:

blocklist:
ports:
- "127.0.0.1:5441:8080"
environment:
Api__TrustedProxies__0: 172.18.0.1

The proxy forwards to http://127.0.0.1:5441 and sends X-Forwarded-For and X-Forwarded-Proto. The address to trust is the one the API sees the proxy's requests come from: for a proxy on the same host that is the gateway of the compose network (docker network inspect <network>). A wrong or missing entry shows itself, as the next section describes.

Who may reach each part of the API can also be narrowed inside the service, door by door: see Firewall and threat protection.

A proxy that is not trusted​

When a proxy on the host's own networks (loopback, or a private address) forwards requests to the console or the API without being named as a trusted proxy, every visitor behind it is judged as the proxy: they share one sign-in limit, and the audit log and the session list show the proxy's address instead of theirs. The console then shows Administrators a notice on every page:

  • which address forwards, whether the console or the API saw it, how many requests, and when it was first and last seen;
  • what to set: Ui__TrustedProxies__0 on the console, or Api__TrustedProxies__0 on the API, with the address it names. Restart that container afterwards;
  • red when the address is loopback (127.0.0.1 or ::1): that is a sidecar or a proxy in the same network namespace, for example peer encryption on Azure Container Apps. Amber for any other private address, which may also be your own forward proxy; trusting it silences the notice.

The notice has no close button. It goes away 24 hours after the proxy was last seen, so once the proxy is trusted it lapses on its own.

The first time a loopback proxy is seen, the service also sends one mail to the recipients on the SMTP page. While the mail cannot be handed to the mail server (no SMTP configured, or the mail server refuses it), it is tried again every 15 minutes, and the notice says so. Once it is handed over it is never sent again, also after a restart.

A request through an SSH tunnel carries no forwarded address, so it never raises the notice.

Entries from the deployment​

Addresses and domains the deployment itself refuses can be set in the API container's environment, Blocklist__Entries__0, Blocklist__Entries__1 and on: a full mail address, or @domain for a domain together with its subdomains. One value may also hold several, separated by commas or semicolons. They are checked like any entry of your own and shown locked on the Blocklists page: they change with the deployment, not through the console or the API. See Your own entries.

Cloudflare Turnstile​

Cloudflare Turnstile confirms that a sign-in comes from a person's browser - it weighs the browser and how it behaves, usually without a puzzle - before the password is even looked at. The console's sign-in page can carry it, so a password or passkey sign-in is accepted only from a browser that passed it. It is off until both of its keys are set, and a console with no internet access leaves it off.

  1. In the Cloudflare dashboard, add a Turnstile widget for the hostname people open the console on, and copy its site key and secret key.
  2. Add them to the console's environment in the compose file, and put the values in .env as BLOCKLIST_TURNSTILE_SITE_KEY, BLOCKLIST_TURNSTILE_SECRET_KEY and, optionally, BLOCKLIST_TURNSTILE_HOSTNAME:
blocklist-ui:
environment:
Turnstile__SiteKey: ${BLOCKLIST_TURNSTILE_SITE_KEY:-}
Turnstile__SecretKey: ${BLOCKLIST_TURNSTILE_SECRET_KEY:-}
Turnstile__Hostnames__0: ${BLOCKLIST_TURNSTILE_HOSTNAME:-}
Turnstile__Bypass: ${BLOCKLIST_TURNSTILE_BYPASS:-false}

Turnstile__Hostnames__0 is optional: when it is set (for example to blocklist.example.com), an answer solved on any other hostname is refused.

The console verifies every answer with Cloudflare, so it must reach challenges.cloudflare.com on port 443. The check fails closed: when Cloudflare cannot be reached, or a key is wrong, nobody can sign in. A refused check counts against the console's sign-in throttle like any other attempt.

Locked out by the check

Start the console with Turnstile__Bypass=true (with the lines above, BLOCKLIST_TURNSTILE_BYPASS=true in .env), or with the start argument --Turnstile:Bypass=true (in compose, command: ["--Turnstile:Bypass=true"] on the console). The check is then off although the keys are set: the console warns about it at every start, and the audit log notes it on every sign-in. Remove it once the cause is fixed.

Settings​

Every setting is listed in the reference. The ones worth knowing on day one:

VariableDefaultWhat it is
Api__Keys__<name>-An accepted API key. At least one is required, and it cannot be revoked through the API
Blocklist__Entries__<n>-The deployment's own entries: full addresses, and @domain for a domain with its subdomains. Shown locked
Blocklist__ScheduledRefreshtruefalse stops every scheduled refresh of the public lists; they are then refreshed only by hand
RateLimit__EnabledfalseWhether the checks are rate limited at all
BLOCKLIST_SEED_ADMIN_PASSgeneratedThe built-in administrator's password. Unset means one is generated on the first start and written to /data/first-admin-password, which only the API's user can read
BLOCKLIST_SEED_ADMIN_TOTP-Optional: the built-in administrator's authenticator secret in base32, at least 26 characters, which makes two-factor required for it; see A required authenticator app