Skip to main content

Setup guide

Suricata dataset blocklist of malicious IPs and domains, reloaded with the rules

Suricata reads local files only, so a cron script on the sensor downloads the full isMalicious lists with your API key and writes them as dataset files. Rules match IP addresses, DNS queries and HTTP hosts against them.

No credit card required · Free API key

Data path
  1. isMalicious

    api.ismalicious.com

    blocklist-ips-critical.txt

    Rebuilt every 12 h

  2. Suricata

    Step 3

    Install the conversion script and run it once

  3. Add the rules

    Step 4
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.

ListEntriesRebuiltPlans
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,000every 12 hBasic, 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,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-ransomware.txtDataset ismalicious-ransomware-domains, type string, on dns.query: domains in the ransomware category.about 3,600every 12 hBasic, 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.

Compare plans

What a download returnsHTTP

GET https://api.ismalicious.com/blocklist/download/blocklist-ips-critical.txt

Full list or 10% sample

FieldBasic, Pro, and EnterpriseFree
X-Blocklist-Version:fulllite
X-Blocklist-Percentage:10010
Total entries:<COUNT><COUNT> (Lite Version - 10% of <TOTAL>)

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, awk and perl.
  • 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

  1. 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.
  2. Store the credentials on the sensor

    Create /etc/ismalicious/netrc root-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>
  3. Install the conversion script and run it once

    • Save the script as /usr/local/sbin/ismalicious-suricata.sh, mode 700, and run it.
    • 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
  4. Add the rules

    Save the rules as /etc/suricata/ismalicious/ismalicious.rules, next to the dataset files: a load path 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 once ismalicious-ips6.lst holds entries. On Suricata 7.0.0 to 7.0.2, remove to_lowercase;. In IPS mode, replace alert with drop.
    /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;)
  5. Add the rule file to suricata.yaml

    Add the rule file to rule-files in suricata.yaml, test the configuration with suricata -T, which loads the rules and datasets without starting capture (with -vvv it 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.rules
    Test 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:'
  6. Schedule the script and a rule reload

    Run the script, then a rule reload, every 12 hours from /etc/cron.d/ismalicious-suricata: the lists are rebuilt every 12 hours, and a reload reads datasets defined with load only from disk again. cron runs with a short PATH, hence the PATH line for suricatasc, which needs the unix socket, on by default; without it, send kill -USR2 to 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 -T ends without dataset errors.
  • suricata -T … -vvv prints dataset: ismalicious-c2-domains loading from for each dataset (the default suricata.log level does not record it), and suricatasc -c ruleset-failed-rules lists no isMalicious rule after a reload.
  • The two IP files together hold the list’s count in /blocklist/stats, give or take one rebuild, and each domain file holds its list’s.
  • dataset-lookup through suricatasc finds a listed domain, given as base64.
  • Resolving a listed C2 domain from a host on $HOME_NET writes an alert with signature_id 1900010 to eve.json.
  • The first lines of a direct download show Total entries without Lite Version.
Checkssh
# 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 -12

Troubleshooting

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.limits.single-hashsize, 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 subdomain with 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.

Get Started

Ready to get started?

No credit card required · Free API key