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
- 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.
- Be Cautious with Wildcards: The syntax
*.domain.com IN CNAME .blocks all subdomains. If your feed contains shared hosting domains like*.github.ioor*.pages.dev, your team’s technical documentation sites will go down with it. That’s why you always need a higher-priority whitelist zone. - 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.

