Skip to main content

Setup guide

pfSense malicious IP blocklist through pfBlockerNG or a URL Table alias

pfSense and pfBlockerNG send no credentials, so a host you control fetches the full isMalicious list with your API key and serves it to the firewall over HTTPS. Load it as a URL Table alias, or as a pfBlockerNG feed, which can refresh every hour and adds DNSBL.

No credit card required · Free API key

Data path
  1. isMalicious

    api.ismalicious.com

    blocklist-ips-critical.txt

    Rebuilt every 12 h

  2. Relay

    Step 3

    Install the fetch script and its timer

  3. pfSense

    Step 5

    Load the IPs as a URL Table alias

On this page08

What you get

pfSense keeps IP lists in pf tables. Firewall Maximum Table Entries is 400,000 by default, and about 160,000 of them already hold the bogon tables when IPv6 is allowed and Block bogon networks is on, both factory defaults. Netgate requires room for twice the total of all tables, so raise it to at least 600,000 before loading the critical IPs. The domain lists go to pfBlockerNG’s DNSBL.

ListEntriesRebuiltPlans
blocklist-ips-critical.txtDefault. IPs listed by 6 or more threat sources, or 3 or more with a critical category: a URL Table alias, or pfBlockerNG IPv4 and IPv6.about 89,000every 12 hBasic, Pro, and Enterprise
blocklist-ips-c2.txtCommand-and-control IPs reported by C2 trackers: a smaller IP list.about 44,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-c2.txtpfBlockerNG DNSBL: command-and-control domains reported by C2 trackers.about 22,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-ransomware.txtpfBlockerNG DNSBL: domains in the ransomware category, whatever their level.about 3,600every 12 hBasic, Pro, and Enterprise
blocklist-domains-cryptomining.txtpfBlockerNG DNSBL: domains in the cryptomining category, whatever their level.about 6,100every 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

  • A Linux host with systemd, curl and an HTTPS web server, which pfSense reaches on your network.
  • pfSense CE or pfSense Plus. For an hourly refresh, IPv6 tabs and DNSBL, the pfBlockerNG or pfBlockerNG-devel package (menus checked against 3.2.15_2 and 3.2.16).
  • For DNSBL: the DNS Resolver (Unbound) enabled, and clients that use pfSense as their DNS server.
  • An isMalicious API key and secret, from Account › API access.
  • Outbound HTTPS (TCP 443) over IPv4 from the relay 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 relay receives the 10% sample, and refuses it.
  2. Store the credentials on the relay host

    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, and pfSense never holds them.
    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 fetch script and its timer

    • Save the script as /usr/local/sbin/ismalicious-mirror, mode 755, with the service and timer units under /etc/systemd/system/, then run the enable block: it creates /var/www/ismalicious, starts the timer and runs the first fetch at once, since the alias step needs the files.
    • The service runs as root to read the netrc, and may write nothing but that directory.
    • Each hour the script downloads every list in LISTS, refuses an error status, a body that is not a list, the 10% sample and a file whose entry count differs from its header, and replaces each file in one move, so the firewall never reads half a file.
    • A refused list keeps its previous copy, and the run then exits 1, so systemctl --failed shows it.
    /usr/local/sbin/ismalicious-mirrorsh
    #!/bin/sh
    # Fetch the lists with your key and publish them for the firewall.
    # A list that fails a check keeps its previous copy, and the run exits 1.
    set -eu
    NETRC=/etc/ismalicious/netrc
    DEST=/var/www/ismalicious
    LISTS="blocklist-ips-critical.txt blocklist-domains-c2.txt"
    
    # 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
    }
    
    mkdir -p "$DEST"
    failed=0
    for f in $LISTS; do
      tmp=$(mktemp "$DEST/.$f.XXXXXX")
      if fetch_list "$f" "$tmp"; then
        chmod 644 "$tmp"
        mv -f "$tmp" "$DEST/$f"
      else
        failed=1
      fi
    done
    exit "$failed"
    /etc/systemd/system/ismalicious-mirror.servicesystemd
    [Unit]
    Description=Fetch isMalicious blocklists
    Wants=network-online.target
    After=network-online.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/ismalicious-mirror
    # Root, to read the root-only netrc; it may write only the published directory.
    NoNewPrivileges=yes
    ProtectSystem=strict
    ProtectHome=yes
    PrivateTmp=yes
    ReadWritePaths=/var/www/ismalicious
    /etc/systemd/system/ismalicious-mirror.timersystemd
    [Unit]
    Description=Fetch isMalicious blocklists every hour
    
    [Timer]
    OnCalendar=hourly
    RandomizedDelaySec=15min
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    Enable the timer and run the first fetchsh
    install -d -m 755 /var/www/ismalicious
    systemctl daemon-reload
    systemctl enable --now ismalicious-mirror.timer
    systemctl start ismalicious-mirror.service   # the first fetch, now
    ls -l /var/www/ismalicious/
  4. Serve the lists over HTTPS on your network

    • Publish /var/www/ismalicious as https://RELAY_HOST/ismalicious/ with a certificate pfSense trusts: a public one, or a private CA added under System › Certificates › Authorities with Add this Certificate Authority to the Operating System Trust Store ticked.
    • Refuse dot files in the web server, since downloads in progress sit there, and allow only the firewall’s address, as in the nginx example below.
    • Keep the relay internal: each file is licensed to the account that downloads it, and its header forbids redistributing the compilation.
    nginx, in the relay’s server blocknginx
    location /ismalicious/ {
        allow <FIREWALL_IP>;
        deny all;
        # Downloads in progress are dot files: never serve them.
        location ~ /\. { deny all; }
    }
  5. Load the IPs as a URL Table alias

    • In System › Advanced › Firewall & NAT, tick Check certificate of aliases URLs, which applies to every URL alias, and set Firewall Maximum Table Entries to 600,000 or more: twice the total of all tables, the bogon tables included.
    • Then Firewall › Aliases › URLs › Add: name isMalicious_IPs, type URL Table (IPs), URL https://RELAY_HOST/ismalicious/blocklist-ips-critical.txt, 1 day in the drop-down after the slash; Save and Apply Changes.
    • On Firewall › Rules › WAN, add a rule with Action Block, Address Family IPv4+IPv6, Protocol Any and Source isMalicious_IPs; on LAN the same with Destination isMalicious_IPs, added with Add rule to the top of the list so it sits above the default allow rule.
    • Tick Log packets that are handled by this rule on both, then Apply Changes.
  6. Or load the lists with pfBlockerNG

    • Enable pfBlockerNG in Firewall › pfBlockerNG › General.
    • In IP, under IP Interface/Rules Configuration, select WAN in Inbound Firewall Rules and LAN in Outbound Firewall Rules, then Save: without them, Deny Both creates no rule.
    • In IP › IPv4 › Add, add a source with Format Auto, State ON, the relay URL of blocklist-ips-critical.txt and the label isMalicious; set the Action to Deny Both and the Update Frequency to every hour or every 12 hours, then repeat in the IPv6 tab with the same source.
    • For domains, tick Enable DNSBL under DNSBL, then DNSBL › DNSBL Groups › Add: Name isMalicious_C2, a source with Format Auto, State ON and the relay URL of blocklist-domains-c2.txt, Action Unbound (the default, Disabled, blocks nothing), Update Frequency every hour; Save.
    • Run Update after each change.

Verify it works

  • On the relay, the entry count of each published file matches the list’s count in /blocklist/stats, give or take one rebuild.
  • Diagnostics › Tables, with isMalicious_IPs selected, shows the entry count and the addresses.
  • Status › System Logs › System › General shows Updated isMalicious_IPs content from after each daily refresh (12:30 by default); Update in Diagnostics › Tables forces one now.
  • With pfBlockerNG, the Update output and the Logs tab report each download, the IP tables appear in Diagnostics › Tables, and DNSBL hits under Reports.
  • The firewall logs show the sessions the Block rules stop.
Relay checkssh
# The relay: next run, last runs, and the entries it published.
systemctl list-timers ismalicious-mirror.timer
journalctl -u ismalicious-mirror.service
grep -c -v '^#' /var/www/ismalicious/blocklist-ips-critical.txt

Troubleshooting

The relay 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 relay keeps the previous file, which pfSense keeps serving. Copy both values again from Account › API access: regenerating the key there invalidates the old pair.

Only about 10% of the entries load

pfSense or pfBlockerNG points straight at api.ismalicious.com: neither has a credential option for a list (a key put in the URL is written to the system log). Or the plan is Free or lapsed: past due, unpaid, canceled, incomplete or paused counts as Free. The relay refuses the sample; a direct feed shows Lite Version in its header.

The relay 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 relay keeps the previous copy and the next run tries again.

The ruleset fails to load

The pf tables are too small: Netgate warns that the firewall may then fail to load the ruleset. Raise Firewall Maximum Table Entries to twice the total of all tables, the bogon tables (about 160,000 entries) included, or load a smaller list.

Timeouts 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.

DNSBL logs every entry as invalid

The # header lines are comments to both parsers, so they are not the cause: an -adguard or -dnsmasq file is. Point DNSBL at the plain domain file.

“Unable to fetch usable data from URL” on save

pfSense downloads the URL when you save the alias. Check the relay URL, its certificate, and that the file exists.

Certificate errors

With Check certificate of aliases URLs ticked, the relay’s certificate must chain to a CA in pfSense’s trust store. In pfBlockerNG, fix the certificate rather than switching the source to FLEX, which stops verifying it after an error.

403 from ismalicious.com

The ismalicious.com edge refuses some sources. Point the relay at api.ismalicious.com.

Limits

  • Neither pfSense nor pfBlockerNG has a credential option for a list (a key put in the URL is written to the system log), so the full list needs the relay host.
  • A URL Table alias refreshes in days, checked once a day. To refresh every hour, use pfBlockerNG.
  • An IP alias resolves every line that looks like a host name through DNS: never point it at a domain list.
  • No CIDR is served: URL Table aliases take addresses and CIDR, not ranges, and the IP lists hold single addresses.
  • DNSBL memory use grows with the list, and pfBlockerNG documents no limit: start with the category lists rather than the 2 million critical domains.
  • The 10% sample is the first tenth of an unsorted file, not the riskiest tenth.

Questions

Can pfSense or pfBlockerNG send an API key?

No. Neither offers a credential field for a list, so pointed straight at api.ismalicious.com they receive the 10% sample. A host you control fetches the full list with your key and serves it to the firewall over HTTPS.

Should I use a URL Table alias or pfBlockerNG?

A URL Table alias is built in and refreshes once a day at most. pfBlockerNG refreshes as often as every hour, takes IPv6 lists in its own tab, and blocks domains through DNSBL.

How often should pfBlockerNG update the lists?

Every hour or every 12 hours. The lists are rebuilt every 12 hours, and the relay fetches them hourly.

Does the critical IP list fit in pfSense’s tables?

Not with the default on a standard install. The 400,000 default is sized for the IPv6 bogon table (about 158,000 entries), and Netgate requires room for twice the total of all tables: with the critical IPs, about 500,000. Set Firewall Maximum Table Entries to 600,000 or more; Netgate budgets about 1 KB of RAM per entry.

Can pfSense block the domain lists?

With pfBlockerNG’s DNSBL, using the plain domain files from the relay. Do not load a domain list into a URL Table alias: pfSense would resolve every name through DNS.

Get Started

Ready to get started?

No credit card required · Free API key