How to block
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.
- 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.
- 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.
- Know how to turn it off. Before you switch a list on, find the one setting that switches it off again.
- 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
| Tool | File | Blocks subdomains? |
|---|---|---|
| Pi-hole (FTL v5.22 or later), AdGuard Home | export/abp.txt | Yes (||name^) |
| dnsmasq | export/dnsmasq.conf | Yes |
| Unbound (including OPNsense) | export/unbound.conf | Yes |
| BIND, PowerDNS Recursor, Knot Resolver (RPZ) | export/rpz.zone | Yes, for registered domains only |
| FortiGate | export/fortigate-domains.txt, export/plain-ips.txt | Yes (*.name) |
| Palo Alto | export/paloalto-domains.txt, export/paloalto-urls.txt, export/plain-ips.txt | Yes (*.name) |
| Microsoft Defender | export/defender-1.csv, export/defender-2.csv, ... | See section 4 |
| One computer's hosts file | export/etc-hosts.txt | No: exact names only |
| Tools that take one hash type or address family | export/hashes-md5.txt, export/hashes-sha1.txt, export/hashes-sha256.txt, export/plain-ipv4.txt, export/plain-ipv6.txt | n/a |
| Scripts that apply only what changed | export/added.txt, export/removed.txt | n/a |
| MISP, OpenCTI, Microsoft Sentinel, other STIX platforms | /feed/stix-bundle.json | n/a |
| Splunk, Sentinel watchlists, Elastic | /feed/lookup.csv | n/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:
- Hand out your DNS server through DHCP.
- On your firewall, block outbound DNS (TCP and UDP port 53) and DNS over TLS (port 853) from everything except your DNS server.
- Browsers can use DNS over HTTPS and skip your server. Turn that off through your browser management policies. Firefox also checks the domain
use-application-dns.net; most of the tools below let you answer it with "no such domain" to switch that off.
Option A: Pi-hole
- In the admin page, open Adlists (Pi-hole v5) or Lists (Pi-hole v6).
- Add
https://watchmanhq.com/feed/blocking/export/abp.txt(or theconfidentone) and save. - Update gravity: Tools > Update Gravity, or
pihole -gon 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
- Filters > DNS blocklists > Add blocklist > Add a custom list.
- Name it
WatchmanHQ blockingand pastehttps://watchmanhq.com/feed/blocking/export/abp.txt. - 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
- Domains: Firewall > pfBlockerNG > DNSBL > DNSBL Groups > Add. Add
https://watchmanhq.com/feed/blocking/domains.txtand.../blocking/hosts.txtas sources (lines starting with#are comments). To block subdomains of the registered domains, turn on wildcard blocking in the DNSBL settings (it needs Unbound's Python mode). That setting applies to every group, so exact hosts then also cover names below them, never above. - IP addresses: Firewall > pfBlockerNG > IP > IPv4 > Add, source
https://watchmanhq.com/feed/blocking/export/plain-ips.txt, action Alias Deny or Deny Outbound. - Set the update frequency to daily or more often.
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
- Security Fabric > External Connectors > Create New > Domain Name. URL:
https://watchmanhq.com/feed/blocking/export/fortigate-domains.txt. Refresh: daily or more often. - In your DNS filter profile, the feed appears under Remote Categories. Start with Monitor, then switch to Block.
- 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
- Objects > External Dynamic Lists > Add. - Type Domain List, source
.../blocking/export/paloalto-domains.txt(it already includes*.nameentries for registered domains). - Type IP List, source.../blocking/export/plain-ips.txt. - Type URL List, source.../blocking/export/paloalto-urls.txt. - Use Test Source URL, then set the check interval to hourly or daily.
- 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.
- Settings > Endpoints > Indicators (under Rules). Choose the tab for the indicator type, then Import > Choose file.
- Import each
defender-N.csv. Defender takes at most 500 indicators per file, which is why there may be several. - The
confidentfiles use Block and severity High. Theblockingfiles use Audit: watch the alerts, then change the action to Block. - 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:
- Defender has no MD5 indicators, so MD5 hashes are left out of these files. They're still in
hashes.txt. - IP address and URL/domain indicators only block in browsers other than Edge when network protection is turned on in block mode.
- If an import is rejected, download the sample CSV from the import page and compare its header line with ours.
5. Threat-intelligence platforms and SIEMs
- STIX 2.1 (
/feed/stix-bundle.json): import the bundle on a schedule. Every indicator carriesvalid_until(the release expiry), a confidence, its source reports, and the labelconfidentwhen it's on Confident. Ids are stable between releases, so re-importing updates indicators instead of duplicating them. - Lookup table (
/feed/lookup.csv): load it as a Splunk lookup, a Sentinel watchlist (search keyvalue) or an Elastic enrich source, then match it against DNS, proxy and firewall logs. It covers the Blocking and Hunting lists.
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
- Start in log or alert mode for one to two weeks, and look at every hit.
- Refresh daily. Every list expires 72 hours after it's built. If your refresh has failed for three days, turn the list off rather than enforce an expired one.
- Verify downloads against
manifest.json(the Unbound script above does). - False positive? Unblock it locally first, then report it.
- Never widen an entry. Don't turn an exact host into a wildcard, or an IP address into a range.
Vendor documentation
Formats were checked against vendor documentation on 2026-10-09: