Skip to main content

Setup guide

Linux firewall IP blocklist loaded into nftables sets by a systemd timer

curl downloads the full isMalicious list with your API key, read from a root-only netrc file. A script loads the addresses into nftables sets with drop rules and replaces them in one transaction; a systemd timer runs it every hour.

No credit card required · Free API key

Data path
  1. isMalicious

    api.ismalicious.com

    blocklist-ips-critical.txt

    Rebuilt every 12 h

  2. Linux nftables

    Step 3

    Install the loader script

On this page08

What you get

The script parses the plain list into nftables sets, limited only by kernel memory. Pick one IP list; the domain lists are for DNS resolvers, not for these sets.

ListEntriesRebuiltPlans
blocklist-ips-critical.txtDefault. 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-ips-critical50k.txtThe 50,000 critical IPs with the highest risk score, for a smaller set.50,000, cappedevery 12 hBasic, Pro, and Enterprise
blocklist-ips-c2.txtCommand-and-control IPs reported by C2 trackers, whatever their level.about 44,000every 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

  • Root access, curl, and nftables 1.0 or later (the nft command, tested with 1.0.6) from your distribution’s packages.
  • systemd 229 or later, which added the timer’s RandomizedDelaySec.
  • An isMalicious API key and secret, from Account › API access.
  • Outbound HTTPS (TCP 443) over IPv4 from the Linux host 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 list needs a Basic, Pro, or Enterprise plan; with a Free key the script receives the 10% sample, and refuses it.
  2. Store the credentials in a netrc file

    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 or in a process listing.
    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 loader script

    • Save the script as /usr/local/sbin/ismalicious-sync, mode 755, and put your management addresses in ALLOW4: they are accepted before any drop.
    • It downloads blocklist-ips-critical.txt, refuses an error status, a body that is not a list, the 10% sample and a file whose entry count differs from its header, then loads the IPv4 and IPv6 addresses into two sets of its own table, inet ismalicious, with drop rules on input, forward and output.
    • nft -c checks the file first, and nft -f replaces the table in one transaction: on any failure the previous table stays in place.
    • To load another list, change the file name in LIST.
    /usr/local/sbin/ismalicious-syncsh
    #!/bin/sh
    # Load the isMalicious IP list into nftables. The table is replaced in one
    # transaction; on any failure the previous one stays in place.
    set -eu
    NETRC=/etc/ismalicious/netrc
    LIST=blocklist-ips-critical.txt
    # IPv4 addresses this host must never drop, such as your management hosts:
    ALLOW4=""
    TMP=$(mktemp -d)
    trap 'rm -rf "$TMP"' 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 "$LIST" "$TMP/list.txt"
    
    OCTET='(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])'
    grep -E "^($OCTET\.){3}$OCTET\$" "$TMP/list.txt" > "$TMP/v4" || true
    grep -E '^[0-9A-Fa-f:]*:[0-9A-Fa-f:]*$' "$TMP/list.txt" > "$TMP/v6" || true
    [ -s "$TMP/v4" ] || { echo "no IPv4 entries" >&2; exit 1; }
    for a in $ALLOW4; do echo "$a"; done > "$TMP/allow4"
    
    elements() {
      if [ -s "$1" ]; then
        printf '        elements = { %s }
    ' "$(paste -sd, "$1")"
      fi
    }
    
    cat > "$TMP/ismalicious.nft" <<NFT
    add table inet ismalicious
    delete table inet ismalicious
    table inet ismalicious {
        set allow4 {
            type ipv4_addr
            flags interval
    $(elements "$TMP/allow4")
        }
        set ism4 {
            type ipv4_addr
            flags interval
            auto-merge
    $(elements "$TMP/v4")
        }
        set ism6 {
            type ipv6_addr
            flags interval
            auto-merge
    $(elements "$TMP/v6")
        }
        chain input {
            type filter hook input priority -10; policy accept;
            ip saddr @allow4 accept
            ip saddr @ism4 drop
            ip6 saddr @ism6 drop
        }
        chain forward {
            type filter hook forward priority -10; policy accept;
            ip saddr @allow4 accept
            ip daddr @allow4 accept
            ip saddr @ism4 drop
            ip daddr @ism4 drop
            ip6 saddr @ism6 drop
            ip6 daddr @ism6 drop
        }
        chain output {
            type filter hook output priority -10; policy accept;
            ip daddr @allow4 accept
            ip daddr @ism4 drop
            ip6 daddr @ism6 drop
        }
    }
    NFT
    nft -c -f "$TMP/ismalicious.nft"
    nft -f "$TMP/ismalicious.nft"
    echo "loaded $(wc -l < "$TMP/v4") IPv4 and $(wc -l < "$TMP/v6") IPv6 entries"
  4. Run it once

    Run the script as root. It prints the number of IPv4 and IPv6 entries loaded, or the reason it refused the list.
    First runsh
    /usr/local/sbin/ismalicious-sync
  5. Refresh it with a systemd timer

    Save the service and timer units under /etc/systemd/system/, then enable the timer. It runs the script after boot, because the table does not survive a reboot, then every hour; RandomizedDelaySec also delays the boot run, which lands between 2 and 17 minutes after boot.
    /etc/systemd/system/ismalicious-sync.servicesystemd
    [Unit]
    Description=Load the isMalicious IP blocklist
    Wants=network-online.target
    After=network-online.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/ismalicious-sync
    /etc/systemd/system/ismalicious-sync.timersystemd
    [Unit]
    Description=Refresh the isMalicious IP blocklist
    
    [Timer]
    OnBootSec=2min
    OnCalendar=hourly
    RandomizedDelaySec=15min
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    Enable the timersh
    systemctl daemon-reload
    systemctl enable --now ismalicious-sync.timer

Verify it works

  • journalctl -u ismalicious-sync.service shows loaded N IPv4 and M IPv6 entries after each run, or the reason the run refused the list.
  • systemctl list-timers ismalicious-sync.timer shows the last and the next run.
  • N plus M matches the list’s count in /blocklist/stats, give or take one rebuild. nft list set can show fewer elements: auto-merge joins adjacent addresses into ranges, and the coverage stays the same.
  • As root, the header check prints X-Blocklist-Version: full; another user cannot read the netrc file, so curl sends no key and the check reads lite. If root sees lite, see “lite list received” under Troubleshooting.
Checkssh
# Next and last runs, and what the last run loaded:
systemctl list-timers ismalicious-sync.timer
journalctl -u ismalicious-sync.service

# The rules, then the first addresses of the IPv4 set:
nft list chain inet ismalicious input
nft list set inet ismalicious ism4 | head -n 20

# As root (the netrc file is root-only): full or lite.
sudo curl -sS -D - -o /dev/null --netrc-file /etc/ismalicious/netrc \
  https://api.ismalicious.com/blocklist/download/blocklist-ips-critical.txt

Troubleshooting

The run prints “download failed”

A 401 means the key or the secret in the netrc file is wrong or incomplete: curl exits with code 22 and the current table stays in place. Check the file: machine api.ismalicious.com, the key as login, the secret as password, mode 600. The response body reads “Blocklist not found or empty”; trust the status code. Regenerating the key in Account › API access invalidates the old pair.

The run prints “lite list received”

No credential reached the server, because of a wrong netrc path or machine name, or the plan is Free or lapsed: past due, unpaid, canceled, incomplete or paused counts as Free. The header check under Verify then prints X-Blocklist-Version: lite.

The run prints “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 current table stays in place; the next run tries again.

Timeouts or a 502 on a large list

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.

“Message too long” from nft

nft runs in an unprivileged container, without CAP_NET_ADMIN in the initial namespace, and cannot raise its socket buffer for a list this size. Run the loader on the host, or in a container with that capability.

Certificate errors

curl checks the certificate against the system CA store: update the ca-certificates package on old systems. Never add --insecure: curl would send the secret to whichever server answers.

403 or 429 from ismalicious.com

The ismalicious.com edge refuses some sources. Use api.ismalicious.com, as the script does.

Limits

  • This guide publishes the nftables loader only. A loader for ipset and iptables has not yet been run on a host with ipset installed, so it is not published.
  • The nftables table does not survive a reboot by itself. The timer reloads it between 2 and 17 minutes after boot; until then the host does not filter on this list.
  • A restart or a reload of nftables.service, or any nft flush ruleset (Debian’s /etc/nftables.conf starts with one), deletes every table, this one included, until the next timer run. Run systemctl start ismalicious-sync.service after reloading it.
  • No CIDR, no ranges and no nftables or iptables-restore file are served: the script builds the sets from the plain list.
  • The 10% sample is the first tenth of an unsorted file, not the riskiest tenth.

Questions

Is there an ipset and iptables version of the script?

Not yet. The ipset loader we drafted has not been run on a host with ipset installed, so this guide publishes the nftables loader only.

Why a netrc file rather than the key on the curl command line?

The curl manual warns that hiding an option from process listings is not enough and advises reading sensitive data from a file. A root-only netrc file keeps the key and the secret out of process listings, shell history and crontabs.

How often should the list be reloaded?

Every hour, as the timer does, and after each boot. The lists are rebuilt every 12 hours, so an hourly check picks up each rebuild soon after it is published.

Does the script block IPv6 addresses?

Yes. An IP list can mix IPv4 and IPv6 addresses, and the script loads each family into its own set, ism4 and ism6, each with its own drop rules.

Does it work alongside firewalld or ufw?

With firewalld and ufw, yes: they rewrite only their own rules, and the script adds its own nftables table, inet ismalicious. In nftables, a packet is accepted only if no rule or base chain policy drops it, so the drop applies whatever the other tables accept. nftables.service is different: a restart, a reload or any nft flush ruleset deletes every table, this one included, until the next timer run.

Get Started

Ready to get started?

No credit card required · Free API key