How to block

Last updated 2026-10-09

A practical guide for small teams. It starts with the leanest option, a DNS filter that protects every device on your network with no software on them, and then covers common firewalls and Microsoft Defender. Every list is rebuilt daily, ready by 10:00 AM US Eastern, and expires 72 hours after it's built, so whatever you set up must refresh itself.

What we check before anything reaches these lists: Methodology.

1. Before you start

Ten minutes here prevents most outages.

  1. Pick a list. Confident is short and has the most evidence: block it. Blocking is longer: run it in log or alert mode for one to two weeks, then block. Hunting is for searching your logs: never block it.
  2. Check against your own traffic. Search your DNS, proxy or firewall logs for the last 30 days for anything on the list you plan to block. A hit is either something to investigate or something you depend on. We can't see your network; you can.
  3. Know how to turn it off. Before you switch a list on, find the one setting that switches it off again.
  4. Read "Trusted services named in attacks" in the daily report. Those are never on our block lists (things like Telegram's API, GitHub, cloud sign-in pages). If you don't use one, blocking it yourself may be worth it.

Where the files are

Every file has an address that never changes and always serves the latest release:

https://watchmanhq.com/feed/<list>/<file>
https://watchmanhq.com/feed/<list>/export/<file>

<list> is confident, blocking or hunting. The Watch page lists every file. Tools that download a URL on a schedule (Pi-hole, AdGuard Home, FortiGate, Palo Alto, pfBlockerNG) can use these directly.

For scripts there's a consistent way to read one release: fetch /feed/CURRENT (a release id), then fetch files from /feed/releases/<id>/ and check them against /feed/releases/<id>/manifest.json. The Unbound script below does exactly that.

Which file for which tool

ToolFileBlocks subdomains?
Pi-hole (FTL v5.22 or later), AdGuard Homeexport/abp.txtYes (||name^)
dnsmasqexport/dnsmasq.confYes
Unbound (including OPNsense)export/unbound.confYes
BIND, PowerDNS Recursor, Knot Resolver (RPZ)export/rpz.zoneYes, for registered domains only
FortiGateexport/fortigate-domains.txt, export/plain-ips.txtYes (*.name)
Palo Altoexport/paloalto-domains.txt, export/paloalto-urls.txt, export/plain-ips.txtYes (*.name)
Microsoft Defenderexport/defender-1.csv, export/defender-2.csv, ...See section 4
One computer's hosts fileexport/etc-hosts.txtNo: exact names only
Tools that take one hash type or address familyexport/hashes-md5.txt, export/hashes-sha1.txt, export/hashes-sha256.txt, export/plain-ipv4.txt, export/plain-ipv6.txtn/a
Scripts that apply only what changedexport/added.txt, export/removed.txtn/a
MISP, OpenCTI, Microsoft Sentinel, other STIX platforms/feed/stix-bundle.jsonn/a
Splunk, Sentinel watchlists, Elastic/feed/lookup.csvn/a

"Blocks subdomains" is about registered domains such as evil.com, where blocking x.evil.com too is right. An exact host such as c2.someone.com is never widened to someone.com in any file.

2. The leanest option: a DNS filter

A DNS filter (sometimes called a sinkhole) answers "no such domain" when a device asks for a malicious name, so the connection never starts. Every device that uses your DNS server is protected: no agents, no licenses.

Make sure devices actually use it

A DNS filter only works if devices can't go around it:

Option A: Pi-hole

  1. In the admin page, open Adlists (Pi-hole v5) or Lists (Pi-hole v6).
  2. Add https://watchmanhq.com/feed/blocking/export/abp.txt (or the confident one) and save.
  3. Update gravity: Tools > Update Gravity, or pihole -g on the command line.

Use abp.txt, not the plain lists: Pi-hole matches a plain domain exactly, so evil.com wouldn't block x.evil.com. Pi-hole usually updates its lists weekly. Schedule it daily (for example a daily cron job running pihole -g), because our lists expire after 72 hours.

Option B: AdGuard Home

  1. Filters > DNS blocklists > Add blocklist > Add a custom list.
  2. Name it WatchmanHQ blocking and paste https://watchmanhq.com/feed/blocking/export/abp.txt.
  3. Set Settings > General settings > Filters update interval to 24 hours or less.

Option C: a Linux server with Unbound

Save this as /usr/local/bin/watchmanhq-unbound.sh and run it daily from cron. It installs a new list only if the file matches its manifest and Unbound accepts the configuration, and otherwise keeps the current one:

#!/bin/sh
# Install the latest WatchmanHQ list for Unbound, only if it verifies.
set -eu
BASE="https://watchmanhq.com/feed"
FILE="blocking/export/unbound.conf"
DEST="/etc/unbound/unbound.conf.d/watchmanhq.conf"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

RELEASE=$(curl -fsS "$BASE/CURRENT")
curl -fsS "$BASE/releases/$RELEASE/manifest.json" -o "$TMP/manifest.json"
curl -fsS "$BASE/releases/$RELEASE/$FILE" -o "$TMP/list"
WANT=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["files"][sys.argv[2]]["sha256"])' "$TMP/manifest.json" "$FILE")
GOT=$(sha256sum "$TMP/list" | cut -d" " -f1)
if [ "$WANT" != "$GOT" ]; then echo "checksum mismatch; keeping the current list" >&2; exit 1; fi
# Every release expires. If the newest one has, updates have stopped:
# stop enforcing the list rather than keep blocking with stale data.
if ! python3 -c 'import json,sys,datetime as d; m=json.load(open(sys.argv[1])); sys.exit(d.datetime.fromisoformat(m["valid_until"]) <= d.datetime.now(d.timezone.utc))' "$TMP/manifest.json"; then
    echo "the newest release has expired; removing the list" >&2
    rm -f "$DEST" && unbound-control reload
    exit 1
fi

[ -f "$DEST" ] && cp "$DEST" "$DEST.previous"
cp "$TMP/list" "$DEST"
if ! unbound-checkconf >/dev/null; then
    echo "Unbound rejected the new list; restoring the previous one" >&2
    [ -f "$DEST.previous" ] && cp "$DEST.previous" "$DEST" || rm -f "$DEST"
    exit 1
fi
unbound-control reload

Make sure /etc/unbound/unbound.conf includes the folder, for example with include: "/etc/unbound/unbound.conf.d/*.conf".

Option D: dnsmasq

Use the same script with FILE="blocking/export/dnsmasq.conf", DEST="/etc/dnsmasq.d/watchmanhq.conf", dnsmasq --test in place of unbound-checkconf, and systemctl restart dnsmasq in place of the reload. Each line is address=/name/, which answers "no such domain" for the name and everything under it.

Option E: BIND or any resolver with RPZ

Fetch rpz.zone with the same script and load it as a response policy zone. In BIND:

response-policy { zone "watchmanhq.rpz"; };
zone "watchmanhq.rpz" { type primary; file "/etc/bind/watchmanhq.rpz"; };

Last resort: one computer, no DNS server

etc-hosts.txt can be added to a computer's hosts file. It blocks exact names only, has no way to expire, and is easy to forget. Prefer any option above.

3. Firewalls

Menu names change between versions; check your version's documentation.

pfSense with pfBlockerNG

OPNsense

OPNsense uses Unbound. Put unbound.conf from option C into /usr/local/etc/unbound.opnsense.d/watchmanhq.conf (the folder recent versions read extra Unbound configuration from; check yours) and restart Unbound. The built-in Services > Unbound DNS > Blocklist page can also take URLs, but check its help text to see whether your version treats entries as exact names or wildcards.

FortiGate

  1. Security Fabric > External Connectors > Create New > Domain Name. URL: https://watchmanhq.com/feed/blocking/export/fortigate-domains.txt. Refresh: daily or more often.
  2. In your DNS filter profile, the feed appears under Remote Categories. Start with Monitor, then switch to Block.
  3. For addresses: External Connectors > IP Address, URL https://watchmanhq.com/feed/blocking/export/plain-ips.txt, and use it as the destination of a deny policy.

Use View Entries on the connector to confirm what was loaded.

Palo Alto Networks

  1. Objects > External Dynamic Lists > Add. - Type Domain List, source .../blocking/export/paloalto-domains.txt (it already includes *.name entries for registered domains). - Type IP List, source .../blocking/export/plain-ips.txt. - Type URL List, source .../blocking/export/paloalto-urls.txt.
  2. Use Test Source URL, then set the check interval to hourly or daily.
  3. Use the domain list in an Anti-Spyware profile's DNS policy (sinkhole), the IP list in a security rule, and the URL list in URL filtering.

4. Endpoints: Microsoft Defender

Microsoft Defender for Endpoint and Defender for Business accept custom indicators.

  1. Settings > Endpoints > Indicators (under Rules). Choose the tab for the indicator type, then Import > Choose file.
  2. Import each defender-N.csv. Defender takes at most 500 indicators per file, which is why there may be several.
  3. The confident files use Block and severity High. The blocking files use Audit: watch the alerts, then change the action to Block.
  4. Each indicator expires at the release's valid-until time, so Defender removes it by itself. Re-import the new files daily, or automate it with the indicators API.

Notes:

5. Threat-intelligence platforms and SIEMs

6. Hunting

The Hunting lists are for searching, not blocking. Search your DNS, proxy and firewall logs for them, and investigate any hit. In Microsoft Defender advanced hunting, for example:

let bad = toscalar(
    externaldata(name:string)
    [h"https://watchmanhq.com/feed/hunting/domains.txt"]
    with (format="txt")
    | where name !startswith "#"
    | summarize make_set(name));
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any (bad)

7. Staying safe

Vendor documentation

Formats were checked against vendor documentation on 2026-10-09: