Setup guide
Suricata dataset blocklist of malicious IPs and domains, reloaded with the rules
No credit card required · Free API key
On this page08
What you get
Each list becomes one dataset file, two for the IPs. A dataset loaded from a file lifts its own memory cap, unless datasets.defaults.memcap is set; its hashsize should stay close to the number of entries.
| List | Entries | Rebuilt | Plans |
|---|---|---|---|
blocklist-ips-critical.txtDefault: datasets ismalicious-ips4 (type ipv4) and ismalicious-ips6 (type ip), on ip.dst and ip.src. IPs listed by 6 or more threat sources, or 3 or more with a critical category. | about 89,000 | every 12 h | Basic, Pro, and Enterprise |
blocklist-domains-c2.txtDataset ismalicious-c2-domains, type string, on dns.query, http.host and tls.sni: command-and-control domains. | about 22,000 | every 12 h | Basic, Pro, and Enterprise |
blocklist-domains-ransomware.txtDataset ismalicious-ransomware-domains, type string, on dns.query: domains in the ransomware category. | about 3,600 | every 12 h | Basic, Pro, and Enterprise |
Entries rounded from the build of ; each list is rebuilt every 12 hours. Today’s counts are public and need no key.
What each plan receives
- FreeFree account, or no key: the first 10% of each list, marked
X-Blocklist-Version: lite. - Basic, Pro, and EnterpriseBasic, Pro, and Enterprise: every list in full.
- Pro and EnterpriseTAXII 2.1 collections, for platforms that read STIX indicators: Pro and Enterprise.
GET https:/
Full list or 10% sample
| Field | Basic, Pro, and Enterprise | Free |
|---|---|---|
X-Blocklist-Version: | full | lite |
X-Blocklist-Percentage: | 100 | 10 |
Total entries: | <COUNT> | <COUNT> |
First lines of the file
# IsMalicious.com Blocklist - IPs (Critical)
# Format: Plain
# Generated: <BUILD_TIME>
# Total entries: <COUNT>
# Update frequency: every 12 hours
# Category: All
# Threat level: Critical
# Website: https://ismalicious.com
# © <YEAR> IsMalicious (compilation). Licensed to the downloading account under https://ismalicious.com/terms; redistribution of the compilation prohibited. Third-party entries remain under their providers' licences — see https://ismalicious.com/sources.
#Values in angle brackets are set by each build.
Prerequisites
- Suricata 7.0 or 8.0, checked against 7.0.17 and 8.0.7. The DNS and TLS rules lowercase the name with
to_lowercase, which needs 7.0.3 or later. - Root on the sensor, with
curl,awkandperl. - An isMalicious API key and secret, from Account › API access.
- Outbound HTTPS (TCP 443) over IPv4 from the sensor to
api.ismalicious.com, which has no IPv6 address.
Set it up
Copy your API key and secret
Open Account › API access and copy the API Key and the API Secret. The full lists need a Basic, Pro, or Enterprise plan; with a Free key the script receives the 10% sample, and refuses it.Store the credentials on the sensor
Create/etc/ismalicious/netrcroot-only with the commands below, then write the three lines into it with an editor: typed in a shell, they would land in its history. curl reads the key and the secret from the file, so they never appear on a command line.Create the file, root-onlysh install -d -m 700 /etc/ismalicious [ -e /etc/ismalicious/netrc ] || install -m 600 /dev/null /etc/ismalicious/netrc chmod 600 /etc/ismalicious/netrc # Then write the three lines below into it with an editor, unless it holds them already./ etc/ ismalicious/ netrcnetrc machine api.ismalicious.com login <API_KEY> password <API_SECRET>Install the conversion script and run it once
- Save the script as
/, mode 700, and run it.usr/ local/ sbin/ ismalicious-suricata. sh - It refuses an error, a body that is not a list, the 10% sample and a file whose entry count differs from its header, drops the
#header lines, and writes the IPv4 and IPv6 addresses to two files, since Suricata 8.0 rejects IPv4 in a type ip dataset, and the base64 of each domain for the string datasets, as Suricata requires. - It then moves the files into
/etc/suricata/ismalicious/; on any failure the files in service stay as they are.
/ usr/ local/ sbin/ ismalicious-suricata. shsh #!/bin/sh # Download isMalicious lists and write Suricata dataset files. On any failure # the files in service stay as they are. set -eu DIR=/etc/suricata/ismalicious NETRC=/etc/ismalicious/netrc mkdir -p "$DIR" # Temporary directory on the same file system, so the final mv is atomic. T=$(mktemp -d "$DIR/.tmp.XXXXXX") trap 'rm -rf "$T"' EXIT # fetch_list LIST OUT: download one list to OUT and check it. # On any failure OUT is removed and the function returns 1. fetch_list() { if ! code=$(curl --silent --show-error --fail --netrc-file "$NETRC" \ --proto '=https' --max-time 300 --retry 2 \ --output "$2" --write-out '%{http_code}' "https://api.ismalicious.com/blocklist/download/$1"); then rm -f "$2"; echo "$1: download failed" >&2; return 1 fi if [ "$code" != 200 ]; then rm -f "$2"; echo "$1: HTTP $code" >&2; return 1 fi if ! head -n 1 "$2" | grep -q '^[#!] IsMalicious.com Blocklist'; then rm -f "$2"; echo "$1: not an isMalicious list" >&2; return 1 fi if grep -q 'Lite Version' "$2"; then rm -f "$2"; echo "$1: lite list received, check the API key and the plan" >&2 return 1 fi # Every entry ends with a newline, and one header counts them: refuse a # cut, doubled or empty file. if [ -n "$(tail -c 1 "$2")" ]; then rm -f "$2"; echo "$1: cut short, no final newline" >&2; return 1 fi if [ "$(grep -c '^[#!] Total entries:' "$2")" != 1 ]; then rm -f "$2"; echo "$1: not one Total entries line" >&2; return 1 fi total=$(sed -n 's/^[#!] Total entries: \([0-9,]*\)$/\1/p' "$2" | tr -d ,) got=$(grep -c '^[^#!]' "$2" || true) if [ -z "$total" ] || [ "$total" = 0 ] || [ "$got" != "$total" ]; then rm -f "$2"; echo "$1: $got entries, the header says ${total:-none}" >&2 return 1 fi } fetch_list blocklist-ips-critical.txt "$T/ips-critical" fetch_list blocklist-domains-c2.txt "$T/domains-c2" fetch_list blocklist-domains-ransomware.txt "$T/domains-ransomware" # Suricata 8.0 reads every line of a type ip set as IPv6 and rejects IPv4 # (OISF bug 9006): IPv4 goes to a type ipv4 set, IPv6 to a type ip set. awk '!/^[#!]/ && NF && !index($1, ":") { print $1 }' "$T/ips-critical" > "$T/ismalicious-ips4.lst" awk '!/^[#!]/ && NF && index($1, ":") { print $1 }' "$T/ips-critical" > "$T/ismalicious-ips6.lst" # Dataset type string: base64 of each domain, no trailing newline in the value. for l in c2 ransomware; do awk '!/^[#!]/ && NF { print $1 }' "$T/domains-$l" \ | perl -MMIME::Base64 -ne 'chomp; print encode_base64($_, ""), "\n"' \ > "$T/ismalicious-$l-domains.lst" done for f in ismalicious-ips4.lst ismalicious-ips6.lst \ ismalicious-c2-domains.lst ismalicious-ransomware-domains.lst; do chmod 644 "$T/$f" mv "$T/$f" "$DIR/$f" done- Save the script as
Add the rules
Save the rules as/, next to the dataset files: aetc/ suricata/ ismalicious/ ismalicious. rules loadpath is relative to the rule file. Take the SIDs from your local range. The IP rules check the client-to-server side of each session only (flow:to_server), so replies do not alert, and alert at most once an hour per address pair; uncomment the IPv6 pair onceismalicious-ips6.lstholds entries. On Suricata 7.0.0 to 7.0.2, removeto_lowercase;. In IPS mode, replacealertwithdrop./ etc/ suricata/ ismalicious/ ismalicious. rulesSuricata # IPv4, both directions (Suricata 7.0 and later) alert ip $HOME_NET any -> $EXTERNAL_NET any (msg:"isMalicious critical IP outbound"; flow:to_server; ip.dst; dataset:isset,ismalicious-ips4, type ipv4, load ismalicious-ips4.lst, hashsize 65536; threshold:type limit, track by_both, count 1, seconds 3600; classtype:bad-unknown; sid:1900001; rev:2;) alert ip $EXTERNAL_NET any -> $HOME_NET any (msg:"isMalicious critical IP inbound"; flow:to_server; ip.src; dataset:isset,ismalicious-ips4, type ipv4, load ismalicious-ips4.lst, hashsize 65536; threshold:type limit, track by_both, count 1, seconds 3600; classtype:bad-unknown; sid:1900002; rev:2;) # IPv6: uncomment when ismalicious-ips6.lst is not empty. # alert ip $HOME_NET any -> $EXTERNAL_NET any (msg:"isMalicious critical IPv6 outbound"; flow:to_server; ip.dst; dataset:isset,ismalicious-ips6, type ip, load ismalicious-ips6.lst, hashsize 4096; threshold:type limit, track by_both, count 1, seconds 3600; classtype:bad-unknown; sid:1900003; rev:1;) # alert ip $EXTERNAL_NET any -> $HOME_NET any (msg:"isMalicious critical IPv6 inbound"; flow:to_server; ip.src; dataset:isset,ismalicious-ips6, type ip, load ismalicious-ips6.lst, hashsize 4096; threshold:type limit, track by_both, count 1, seconds 3600; classtype:bad-unknown; sid:1900004; rev:1;) # Domains (on Suricata 7.0.0 to 7.0.2, remove "to_lowercase;") alert dns $HOME_NET any -> any any (msg:"isMalicious C2 domain in DNS query"; dns.query; to_lowercase; dataset:isset,ismalicious-c2-domains, type string, load ismalicious-c2-domains.lst, hashsize 32768; classtype:trojan-activity; sid:1900010; rev:1;) alert http $HOME_NET any -> any any (msg:"isMalicious C2 domain in HTTP Host"; flow:established,to_server; http.host; dataset:isset,ismalicious-c2-domains, type string, load ismalicious-c2-domains.lst, hashsize 32768; classtype:trojan-activity; sid:1900011; rev:1;) alert dns $HOME_NET any -> any any (msg:"isMalicious ransomware domain in DNS query"; dns.query; to_lowercase; dataset:isset,ismalicious-ransomware-domains, type string, load ismalicious-ransomware-domains.lst, hashsize 4096; classtype:trojan-activity; sid:1900012; rev:1;) alert tls $HOME_NET any -> any any (msg:"isMalicious C2 domain in TLS SNI"; flow:established,to_server; tls.sni; to_lowercase; dataset:isset,ismalicious-c2-domains, type string, load ismalicious-c2-domains.lst, hashsize 32768; classtype:trojan-activity; sid:1900013; rev:1;)Add the rule file to suricata.yaml
Add the rule file torule-filesinsuricata.yaml, test the configuration withsuricata -T, which loads the rules and datasets without starting capture (with-vvvit also prints each dataset as it loads), then restart Suricata once.suricata. yamlyaml # Merge into the existing key: rule-files: - suricata.rules - /etc/suricata/ismalicious/ismalicious.rulesTest the configurationsh suricata -T -c /etc/suricata/suricata.yaml # The datasets as loaded, at config verbosity: suricata -T -c /etc/suricata/suricata.yaml -vvv 2>&1 | grep 'dataset:'Schedule the script and a rule reload
Run the script, then a rule reload, every 12 hours from/: the lists are rebuilt every 12 hours, and a reload reads datasets defined withetc/ cron. d/ ismalicious-suricata loadonly from disk again. cron runs with a short PATH, hence the PATH line forsuricatasc, which needs the unix socket, on by default; without it, sendkill -USR2to Suricata instead./ etc/ cron. d/ ismalicious-suricatacron PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 17 3,15 * * * root /usr/local/sbin/ismalicious-suricata.sh && suricatasc -c reload-rules
Verify it works
suricata -Tends without dataset errors.suricata -T … -vvvprintsdataset: ismalicious-c2-domainsfor each dataset (the defaultloading from suricata.loglevel does not record it), andsuricatasclists no isMalicious rule after a reload.-c ruleset-failed-rules - The two IP files together hold the list’s
countin /blocklist/stats, give or take one rebuild, and each domain file holds its list’s. dataset-lookupthroughsuricatascfinds a listed domain, given as base64.- Resolving a listed C2 domain from a host on
$HOME_NETwrites an alert withsignature_id1900010 toeve.json. - The first lines of a direct download show
Total entrieswithoutLite Version.
# Entries per dataset file:
wc -l /etc/suricata/ismalicious/*.lst
# Rules the last reload dropped (none expected):
suricatasc -c ruleset-failed-rules
# Is a domain in the dataset? (unix socket, 7.0 and 8.0)
suricatasc -c "dataset-lookup ismalicious-c2-domains string $(printf '%s' <LISTED_DOMAIN> | base64 -w0)"
# As root (the netrc file is root-only): the key reaches isMalicious when
# Total entries has no "Lite Version".
sudo curl -sS --netrc-file /etc/ismalicious/netrc \
https://api.ismalicious.com/blocklist/download/blocklist-ips-critical.txt | head -12Troubleshooting
The script stops with a 401
The key or the secret in the netrc file is wrong or incomplete: curl exits with code 22, the script stops before touching the live files, and Suricata keeps the previous ones. The response body reads “Blocklist not found or empty”; trust the status code. Regenerating the key in Account › API access invalidates the old pair.
“lite list received”
No credential reached the server, or the plan is Free or lapsed: past due, unpaid, canceled, incomplete or paused counts as Free. Check the netrc entry and the plan.
“entries, the header says”, “cut short, no final newline” or “not one Total entries line”
The file was empty, cut short or not one of the lists, as a proxy or captive portal answers. The script stops before it touches the files in service; the next run tries again.
Timeouts or a 502
A download that takes longer than 30 seconds on our side ends in a timeout or a 502: use a tier or category list from the table.
503 with Retry-After: 600
A list is being built. The next run picks it up.
“invalid Ipv6 value” for ismalicious-ips
Suricata 8.0 reads every line of a type ip dataset as IPv6 and rejects IPv4 addresses (OISF bug 9006); suricata -T then fails. Keep IPv4 addresses in the type ipv4 set the script writes, ismalicious-ips4.lst.
“bad base64 encoding” or “invalid Ipv4 value”
A string dataset holds plain domains instead of base64, or a rule loads the wrong file into a set (on 7.0 the message is “dataset data parse failed”). Use the script’s files, each with its own rule.
“hashsize … exceeds configured 'single-hashsize' limit”
Lower hashsize to 65,536 or less on 8.0 (262,144 on 7.0.9 and later), or raise datasets., which takes a restart.
“dataset … load mismatch”
Two rules use the same dataset name with different load files. Keep one file per name.
No DNS alerts on Suricata 7.0.0 to 7.0.2
The query was not lowercase: to_lowercase exists from 7.0.3, and the lists hold lowercase names. Upgrade, or accept that mixed-case queries do not match.
curl exits with code 60
Update the CA bundle, check the clock, and exempt api.ismalicious.com from TLS inspection.
Limits
- Datasets match the listed value only. Subdomain matching needs
match subdomainwith dot-prefixed entries, which exists from Suricata 8.0.7 (absent in 8.0.6); these files hold exact names. - IP reputation (
iprep) is not covered: each entry takes a host-table slot whose size Suricata does not document, and a full table stops loading the file. - A reload builds a second detection engine before freeing the first, so the datasets are held twice for a moment.
- No CIDR, RPZ, CSV or JSON files are served: the script builds the files Suricata needs from the plain lists.
- The hash lists are not covered: Suricata’s file-hash keywords need file inspection settings this guide has not verified.
- The 10% sample is the first tenth of an unsorted file, not the riskiest tenth: the script refuses it.
Questions
Can Suricata download a blocklist by itself?
No. Suricata reads local files only. A cron script on the sensor downloads the isMalicious lists with curl, writes them as dataset files, and a rule reload loads them.
Why are the domain files base64-encoded?
Suricata’s string datasets take one base64 value per line, in both 7.0 and 8.0. The script encodes each domain without its trailing newline, so the value matches the name in the traffic.
Why datasets rather than IP reputation?
IP reputation keeps each address in the host table, whose memory per entry Suricata does not document, and a full table stops loading the file. A dataset loaded from a file lifts its own memory cap.
Does each update need a Suricata restart?
No. A rule reload, through suricatasc or a USR2 signal, reads datasets defined with load only from disk again. Restart only after changing suricata.yaml.
Related
Lists as lookups, ES threat lists, HEC push
TAXII 2.1 collections for indicator match rules
Threat IPs and domains as CDB lists for Wazuh rules
Threat IPs and domains as QRadar reference sets
TAXII 2.1 indicators into Sentinel
Every list, level and category
TAXII 2.1 collections for SIEMs and TIPs
Authentication, endpoints and limits
Check one domain before you block it
Get Started
Ready to get started?
No credit card required · Free API key