Setup guide
Linux firewall IP blocklist loaded into nftables sets by a systemd timer
No credit card required · Free API key
isMalicious
api.ismalicious.com
blocklist-ips-critical.txt
Rebuilt every 12 h
Linux nftables
Step 3Install 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.
| List | Entries | Rebuilt | Plans |
|---|---|---|---|
| blocklist-ips-critical.txtDefault. 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-ips-critical50k.txtThe 50,000 critical IPs with the highest risk score, for a smaller set. | 50,000, capped | every 12 h | Basic, Pro, and Enterprise |
| blocklist-ips-c2.txtCommand-and-control IPs reported by C2 trackers, whatever their level. | about 44,000 | 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
- Root access, curl, and nftables 1.0 or later (the
nftcommand, 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
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.Store the credentials in a netrc file
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 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>Install the loader script
- Save the script as
/, mode 755, and put your management addresses inusr/ local/ sbin/ ismalicious-sync 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 -cchecks the file first, andnft -freplaces 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"- Save the script as
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-syncRefresh 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;RandomizedDelaySecalso 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.targetEnable the timersh systemctl daemon-reload systemctl enable --now ismalicious-sync.timer
Verify it works
journalctlshows-u ismalicious-sync. service loadedafter each run, or the reason the run refused the list.N IPv4 and M IPv6 entries systemctlshows the last and the next run.list-timers ismalicious-sync. timer - N plus M matches the list’s
countin /blocklist/stats, give or take one rebuild.nft list setcan show fewer elements:auto-mergejoins 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 readslite. If root seeslite, see “lite list received” under Troubleshooting.
# 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.txtTroubleshooting
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 anynft flush ruleset(Debian’s/etc/nftables.confstarts with one), deletes every table, this one included, until the next timer run. Runsystemctlafter reloading it.start ismalicious-sync. service - 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.
Related
IP and domain feeds with basic authentication
External Dynamic Lists sized to PAN-OS limits
A URL Table alias or pfBlockerNG feed through a relay
URL Table aliases with Basic authorization
Threat domains as a RouterOS DNS adlist
Keep firewall blocklists current
Every list, level and category
TAXII 2.1 collections for SIEMs and TIPs
Authentication, endpoints and limits
Check one address before you block it
Get Started
Ready to get started?
No credit card required · Free API key