Updated October 2026: ThreatFox now requires a free Auth-Key on every API request, and returns at most the last seven days of IOCs. The commands below are updated for both and were re-tested on Suricata 7.0 and 8.0.
In our previous post, we talked about why you should use Suricata IDS to alert on IOCs. Suricata’s Datasets feature is what makes that practical: one rule can match traffic against a list of thousands, or millions, of Indicators of Compromise (IOCs), such as malicious domains, IP addresses and file hashes.
It works in a simple way. You write the indicators to a file, Base64-encoded for strings such as domains and in hex for MD5 and SHA256 hashes, and a Suricata rule loads that file and matches traffic against it.
In this post we’ll use Datasets to alert on DNS queries to malicious domains published by abuse.ch ThreatFox.
Before you start: get a ThreatFox Auth-Key
ThreatFox requires an Auth-Key header on every API request; without one, the API answers 401 Unauthorized. Keys are free: sign in to the abuse.ch Authentication Portal and generate one from your profile.
You’ll also need curl and jq:
sudo apt install -y curl jq
Preparing the IOCs file
Put your key in a variable, then download the last seven days of IOCs from the ThreatFox API:
export THREATFOX_AUTH_KEY='your-auth-key'
curl -s -X POST https://threatfox-api.abuse.ch/api/v1/ \
-H "Auth-Key: ${THREATFOX_AUTH_KEY}" \
-d '{ "query": "get_iocs", "days": 7 }' > iocs_list.json
days accepts 1 to 7; any other value returns illegal_days. For older indicators, ThreatFox publishes export files, which need the same key.
Check that ThreatFox accepted the request before going further. Without a key you get {"error": "Unauthorized"}, and with a key ThreatFox doesn’t recognise, {"query_status": "unknown_auth_key"}; neither contains any IOCs:
jq -e '.query_status == "ok"' iocs_list.json
It prints true and exits with 0 on a good response, which also makes it a handy guard in scripts.
Now extract the domain IOCs:
jq -r '.data[] | select(.ioc_type == "domain") | .ioc' iocs_list.json | sort -u > domains.txt
Suricata expects a string dataset to be Base64-encoded, one entry per line, so encode each domain:
while IFS= read -r domain; do printf '%s' "$domain" | base64 -w 0; echo; done < domains.txt > threatfox_domains.lst
Finally, copy the list next to your rules:
sudo mkdir -p /etc/suricata/rules
sudo install -m 644 threatfox_domains.lst /etc/suricata/rules/
Creating the Suricata rule
To alert on DNS queries for any domain in the list, the rule compares the dns.query buffer with the dataset:
alert dns $HOME_NET any -> any any (msg:"ThreatFox IOC: DNS query for a malicious domain"; dns.query; dataset:isset,threatfox_domains,type string,load /etc/suricata/rules/threatfox_domains.lst,memcap 10mb,hashsize 4096; classtype:trojan-activity; sid:1000001; rev:1;)
issetraises the alert when the queried name is in the set.type stringwithloadreads the Base64 list we just built. If you load hundreds of thousands of domains, raisememcapandhashsizeto match.sid:1000001is in the range reserved for local rules, so it won’t collide with your ruleset.
We define the dataset in the rule rather than in suricata.yaml on purpose. Suricata re-reads a dataset that a rule only loads every time the rules are reloaded. Sets defined in suricata.yaml are not reloaded; you would have to update them entry by entry with the dataset-add and dataset-remove unix socket commands.
Save the rule in its own file:
sudo tee /etc/suricata/rules/iocs.rules > /dev/null <<'EOF'
alert dns $HOME_NET any -> any any (msg:"ThreatFox IOC: DNS query for a malicious domain"; dns.query; dataset:isset,threatfox_domains,type string,load /etc/suricata/rules/threatfox_domains.lst,memcap 10mb,hashsize 4096; classtype:trojan-activity; sid:1000001; rev:1;)
EOF
Then add that file to rule-files in /etc/suricata/suricata.yaml. Use its absolute path and leave default-rule-path as it is, because suricata-update keeps writing suricata.rules there (/var/lib/suricata/rules on current packages):
default-rule-path: /var/lib/suricata/rules
rule-files:
- suricata.rules
- /etc/suricata/rules/iocs.rules
Test the configuration and restart Suricata:
sudo suricata -T -c /etc/suricata/suricata.yaml
sudo systemctl restart suricata
suricata -T also fails if suricata.rules doesn’t exist yet. If you have never run suricata-update, run sudo suricata-update first.
Verifying that alerting works
You don’t need to query a real malicious domain from your network to test this. Add a harmless test entry to the list, and reload the rules so Suricata re-reads it:
echo "$(printf '%s' 'threatfox-ioc-test.example' | base64 -w 0)" | sudo tee -a /etc/suricata/rules/threatfox_domains.lst
sudo systemctl reload suricata
Then look the name up. It doesn’t exist, which is fine: Suricata only needs to see the query. (nslookup comes with the dnsutils package.)
nslookup threatfox-ioc-test.example
The alert lands in /var/log/suricata/eve.json:
sudo jq 'select(.event_type == "alert" and .alert.signature_id == 1000001)' /var/log/suricata/eve.json
Here is a shortened alert from our Suricata 8.0 test, with the addresses changed. Suricata 7.0 logs the name under dns.query instead of dns.queries.
{
"timestamp": "2026-10-08T03:01:35.146914+0000",
"event_type": "alert",
"src_ip": "192.168.1.20",
"src_port": 60871,
"dest_ip": "192.168.1.1",
"dest_port": 53,
"proto": "UDP",
"alert": {
"action": "allowed",
"signature_id": 1000001,
"signature": "ThreatFox IOC: DNS query for a malicious domain",
"category": "A Network Trojan was detected",
"severity": 1
},
"dns": {
"version": 3,
"type": "request",
"queries": [
{ "rrname": "threatfox-ioc-test.example", "rrtype": "A" }
]
},
"app_proto": "dns"
}
Suricata alerted on the listed domain. The same rule scales to millions of IOCs that would otherwise take tens of thousands of separate rules. Rebuilding the list with the steps above also removes the test entry.
How IDSTower can help
The list you just built goes stale fast: ThreatFox adds new IOCs all the time, and the API only returns the last seven days. Keeping it current, on every sensor, is what IDSTower does for you:
- ThreatFox is built in. Enable the abuse.ch ThreatFox feed under Settings → Feeds and add your Auth-Key; IDSTower downloads the IOCs from then on (how to enable a feed).
- Every host stays current. New indicators reach every managed Suricata host automatically. MISP, TAXII/STIX and your own CSV or JSON feeds work the same way (threat intelligence feeds).
- The rules are written for you. For domains, IDSTower alerts on DNS queries, HTTP Host headers and TLS SNI. For SHA256 hashes, it adds file-hash rules for HTTP, HTTP/2, SMTP, FTP, SMB and NFS. Each indicator’s score becomes the alert’s priority.
Try it in the live demo (no signup), or get a free license.
