Configuring a DNS Firewall with Response Policy Zones (RPZ) on BIND9: Block Malware and Phishing Effectively

Network tutorial - IT technology blog
Network tutorial - IT technology blog

When Traditional Firewalls Fall Short

Over 6 months ago, our 50-workstation office experienced an unforgettable incident. An employee accidentally clicked on a banking phishing link in an email. Immediately after, malware on the machine began relentlessly querying DNS for suspicious Dynamic DNS domains to download a secondary payload. Our perimeter firewall had IDS/IPS enabled. However, because all traffic was TLS/HTTPS encrypted, the firewall struggled to process it quickly enough and failed to block it in time.

The most effective solution at that point was blocking it right at the domain resolution layer (internal DNS Resolver). You don’t need to spend tens of thousands of dollars on expensive Next-Gen Firewalls. You also don’t need to install heavy agents on every employee’s machine. BIND9 comes with an extremely powerful built-in weapon: Response Policy Zones (RPZ). This feature turns a standard DNS server into a robust DNS Firewall, severing malware communications right at the IP lookup phase.

How Response Policy Zones (RPZ) Work

In simple terms, RPZ acts as an intermediary filtering layer during the recursive DNS resolution process. When a client sends a query, BIND9 still queries Root and Authoritative DNS servers on the Internet as usual. But before returning the response to the client workstation, BIND9 checks the result against a local "ruleset" you define.

Common Policy Actions in RPZ

  • NXDOMAIN: Returns a Name Error indicating the domain does not exist. The malicious domain effectively vanishes from the internal network, immediately breaking the malware’s connection.
  • NODATA: Indicates the domain exists but has no matching A/AAAA records.
  • Redirect / Sinkhole (Local Data): Points the query to an internal Web Server IP (e.g., 10.10.10.254). This page displays a security warning to employees or logs the request to isolate the infected machine.
  • PASSTHRU: Allows the DNS query to pass through normally. This action is used to quickly whitelist critical business domains that were falsely blocked.
  • DROP: Drops the query packet, causing the client to time out (rarely used because it can freeze user-facing applications).

7 Steps to Build a DNS Firewall with BIND9 RPZ

Step 1: Enable the response-policy Feature in BIND9

First, open BIND9’s main configuration file (/etc/bind/named.conf.options on Ubuntu/Debian, or /etc/named.conf on CentOS/RHEL). Then, add the response-policy block inside the options clause:

sudo nano /etc/bind/named.conf.options
options {
    directory "/var/cache/bind";

    // Enable recursive DNS for LAN subnets
    recursion yes;
    allow-query { 127.0.0.1; 192.168.1.0/24; 10.10.0.0/16; };
    allow-recursion { 127.0.0.1; 192.168.1.0/24; 10.10.0.0/16; };

    // Define RPZ zones list
    response-policy {
        zone "rpz.whitelist.local" policy passthru;
        zone "rpz.malware.local"   policy given;
    } qname-wait-recurse no;

    dnssec-validation auto;
    listen-on-v6 { any; };
};

Note: The declaration order determines priority. BIND9 evaluates rules from top to bottom. The rpz.whitelist.local zone must always come first so legitimate business domains are never accidentally blocked.

Step 2: Declare RPZ Zones in named.conf.local

Declare the zone definitions just like regular DNS zones in /etc/bind/named.conf.local:

sudo nano /etc/bind/named.conf.local
// Whitelist RPZ Zone
zone "rpz.whitelist.local" {
    type master;
    file "/etc/bind/rpz/db.rpz.whitelist";
    allow-query { localhost; };
    allow-transfer { none; };
};

// Malware & Phishing Blacklist RPZ Zone
zone "rpz.malware.local" {
    type master;
    file "/etc/bind/rpz/db.rpz.malware";
    allow-query { localhost; };
    allow-transfer { none; };
};

Step 3: Create Zone Files and Define Filter Rules

Create a dedicated directory to manage RPZ files and assign ownership to the service user:

sudo mkdir -p /etc/bind/rpz
sudo chown -R bind:bind /etc/bind/rpz

The whitelist file /etc/bind/rpz/db.rpz.whitelist bypasses trusted domains:

$TTL 300
@       IN      SOA     localhost. root.localhost. (
                        2024040101      ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        300 )           ; Minimum
        IN      NS      localhost.

; Allow normal resolution even if domain is in a blacklist
github.com              IN      CNAME   rpz-passthru.
*.github.com            IN      CNAME   rpz-passthru.

The malware blocking file /etc/bind/rpz/db.rpz.malware defines policy action rules:

$TTL 300
@       IN      SOA     localhost. root.localhost. (
                        2024040101      ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        300 )           ; Minimum
        IN      NS      localhost.

; 1. Block phishing domains with NXDOMAIN (dot indicates null root)
bad-phishing-site.xyz   IN      CNAME   .
*.bad-phishing-site.xyz IN      CNAME   .

; 2. Block C2 servers with NODATA (asterisk followed by dot)
malware-c2-server.top   IN      CNAME   *.
*.malware-c2-server.top IN      CNAME   *.

; 3. Redirect (Sinkhole) to internal warning page
trojan-download.online  IN      A       10.10.10.254
*.trojan-download.online IN     A       10.10.10.254

Step 4: Configure a Dedicated Log Channel for RPZ

When a client queries a malicious domain, you need to identify that IP precisely to isolate the device promptly. Separate the rpz category into an independent log file in /etc/bind/named.conf.log:

sudo nano /etc/bind/named.conf.log
logging {
    channel rpz_log {
        file "/var/log/named/rpz.log" versions 5 size 20m;
        severity info;
        print-time yes;
        print-category yes;
        print-severity yes;
    };
    category rpz {
        rpz_log;
    };
};

Don’t forget to create the log directory and grant write permissions to the DNS daemon:

sudo mkdir -p /var/log/named
sudo chown -R bind:bind /var/log/named

Step 5: Check Syntax and Apply Configuration

Before reloading BIND9, thoroughly verify the syntax to avoid disrupting network-wide DNS resolution:

# Check main configuration file
sudo named-checkconf

# Check individual zone files
sudo named-checkzone rpz.malware.local /etc/bind/rpz/db.rpz.malware

# Apply configuration safely without disconnecting clients
sudo rndc reload

Step 6: Test from a Client Machine

From a workstation on the LAN (e.g., 192.168.1.45), use dig to test-query the malicious domain:

dig @192.168.1.2 bad-phishing-site.xyz

The output will immediately show an NXDOMAIN status without an ANSWER SECTION:

;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 48215
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

Open the log file on the DNS Server to view the details:

sudo tail -f /var/log/named/rpz.log
01-Apr-2024 14:22:10.104 rpz: info: client @0x7f88a0 192.168.1.45#51234 (bad-phishing-site.xyz): rpz QNAME NXDOMAIN rewrite bad-phishing-site.xyz via bad-phishing-site.xyz.rpz.malware.local

The log entry clearly indicates that host 192.168.1.45 just accessed a phishing domain. You can use this information to isolate and remediate the incident.

Step 7: Automate Threat Feed Updates

Adding domains manually is impractical. In a production environment, write a small bash script triggered by a cron job every 2 hours to pull and update feeds from URLhaus or MalwareDomainList:

#!/bin/bash
# Script: /usr/local/bin/update-rpz.sh
set -euo pipefail

FEED_URL="https://urlhaus.abuse.ch/downloads/rpz/"
DEST_FILE="/etc/bind/rpz/db.rpz.urlhaus"
TMP_FILE="/tmp/urlhaus.rpz"

# Download new feed
if curl -s -f -L "$FEED_URL" -o "$TMP_FILE"; then
    # Validate zone file before overwriting
    if named-checkzone rpz.urlhaus.local "$TMP_FILE" > /dev/null 2>&1; then
        mv "$TMP_FILE" "$DEST_FILE"
        chown bind:bind "$DEST_FILE"
        rndc reload rpz.urlhaus.local
    else
        rm -f "$TMP_FILE"
    fi
fi

3 Hard-Learned Lessons from 6 Months of Operation

  1. Always Enforce a Low $TTL (300s): If you mistakenly block an essential partner website, you simply remove the rule. Clients will pick up the change within 5 minutes instead of facing network disruptions all day.
  2. Be Cautious with Wildcards: The syntax *.domain.com IN CNAME . blocks all subdomains. If your feed contains shared hosting domains like *.github.io or *.pages.dev, your team’s technical documentation sites will go down with it. That’s why you always need a higher-priority whitelist zone.
  3. Extremely Low Resource Overhead: Loading a zone with over 100,000 malicious domains consumes only ~70MB of additional RAM on BIND9. DNS response latency increases by less than 1ms, making the delay completely unnoticeable to end users.

With just a few configuration lines in BIND9, your office network gains an active layer of defense, stopping botnets and phishing threats right at the gateway.

Share: