What Are Response Policy Zones?
Response Policy Zones (RPZ) is a DNS firewall mechanism built into BIND9 that intercepts DNS queries and applies configurable policies before returning responses to clients. Originally developed by Paul Vixie and Vernon Schryver at ISC (Internet Systems Consortium), RPZ allows DNS administrators to block malicious domains, redirect phishing sites to a sinkhole, enforce acceptable use policies, and integrate threat intelligence feeds — all at the DNS layer, without touching firewall rulesets or endpoint configurations.
At its core, RPZ works by defining a special DNS zone that acts as a policy database. When a client sends a query to your BIND9 resolver, the server checks the query against all configured RPZ zones before performing recursive resolution. If a match is found, the configured action — block, redirect, passthrough, or silent drop — is applied immediately and the synthesized response is returned to the client.
Why DNS Firewalling with RPZ Matters
Blocking threats at the DNS layer offers several operational advantages over traditional IP-based firewall rules or endpoint security agents:
- Early interception: DNS is queried before any network connection is established, stopping command-and-control callbacks and malware downloads before the first packet is sent
- Domain-name awareness: RPZ blocks by fully-qualified domain name, wildcard subdomain patterns, or nameserver associations — not just by IP address, which threat actors rotate constantly
- Centralized enforcement: A single BIND9 resolver with RPZ protects every client on your network without per-host software deployment
- Feed integration: Commercial and open-source threat intelligence providers publish RPZ-formatted zone files that BIND9 can consume via standard zone transfers
- Audit trail: Every RPZ policy match generates a structured log entry, giving security teams a clear record of blocked query attempts for incident response and compliance
Prerequisites
- BIND9 9.8 or later (RPZ support was introduced in 9.8; BIND9 9.11 or later is recommended for full trigger and action support)
- Ubuntu 20.04 or 22.04 with BIND9 installed and configured as a recursive resolver
- Write access to
/etc/bind/named.conf.options
,/etc/bind/named.conf.local
, and the zone file directory - Root or sudo privileges on sw-infrarunbook-01
Verify your installed BIND9 version before proceeding:
named -v
Step 1 – Enable RPZ in the BIND9 Options Block
Open the main BIND9 options file. On Ubuntu this is
/etc/bind/named.conf.options:
sudo nano /etc/bind/named.conf.options
Add the
response-policydirective inside the
options { };block, after your
forwardersand
dnssec-validationsettings:
options {
directory "/var/cache/bind";
forwarders {
10.0.0.1;
10.0.0.2;
};
dnssec-validation auto;
listen-on { 127.0.0.1; 10.10.1.53; };
allow-recursion { 10.10.0.0/16; 127.0.0.1; };
recursion yes;
response-policy {
zone "rpz.solvethenetwork.com" policy given;
};
};
The
policy givendirective instructs BIND9 to honour whatever action is specified in each individual RPZ record within the zone. Other global policy overrides such as
NXDOMAIN,
NODATA,
PASSTHRU, or
DROPforce a single action for every match in that zone regardless of zone record content. The
givensetting is the most flexible choice for production deployments where you need mixed actions across different categories of blocked domains.
Step 2 – Declare the RPZ Zone in named.conf.local
Add the zone declaration to
/etc/bind/named.conf.local. The RPZ zone is a standard primary zone with strict access controls — clients must never be able to query it directly, and it must not be transferable to unauthorized systems:
zone "rpz.solvethenetwork.com" {
type primary;
file "/etc/bind/zones/rpz.solvethenetwork.com.db";
allow-query { none; };
allow-transfer { none; };
};
Create the zones directory and assign the correct ownership:
sudo mkdir -p /etc/bind/zones
sudo chown bind:bind /etc/bind/zones
Step 3 – Create the RPZ Zone File
Create the zone file at the path you declared above. RPZ zone files use standard DNS zone file syntax. The owner names and record types encode policy triggers and actions using special BIND9 conventions:
sudo nano /etc/bind/zones/rpz.solvethenetwork.com.db
Start with a minimal working zone that demonstrates the most common use case — returning NXDOMAIN for known malicious domains:
$TTL 300
@ IN SOA sw-infrarunbook-01.solvethenetwork.com. infrarunbook-admin.solvethenetwork.com. (
2024031501 ; Serial (YYYYMMDDNN)
3600 ; Refresh
900 ; Retry
604800 ; Expire
300 ) ; Negative cache TTL
IN NS sw-infrarunbook-01.solvethenetwork.com.
; ============================================================
; QNAME Triggers -- block queries for specific domain names
; ============================================================
; Block a known malware distribution domain -- return NXDOMAIN
badactor-malware.net CNAME .
; Block all subdomains of a phishing domain -- return NXDOMAIN
*.phish-campaign.ru CNAME .
; Block apex and all subdomains of a C2 domain
cnc-botnet.xyz CNAME .
*.cnc-botnet.xyz CNAME .
; ============================================================
; Walled Garden -- redirect to sinkhole instead of NXDOMAIN
; ============================================================
; Redirect an ad-tracking domain to internal sinkhole at 10.10.9.99
adtracker-network.com A 10.10.9.99
*.adtracker-network.com A 10.10.9.99
The short
$TTL 300at the top applies a 5-minute TTL to all RPZ records. This ensures that when you remove a block entry, resolvers and clients flush the cached policy quickly without requiring a full restart.
Understanding RPZ Trigger Types
QNAME Triggers
The simplest and most widely used trigger. The owner name in the RPZ zone is the domain being queried by the client. When a client queries
badactor-malware.net, BIND9 finds a matching QNAME entry and applies the action immediately. Wildcard entries such as
*.phish-campaign.rumatch all subdomains but do not match the apex itself — this is why production blocklists include both the bare domain and a wildcard entry for complete coverage.
IP Address Response Triggers (rpz-ip)
Triggers on IP addresses returned in DNS responses from the upstream resolver. This is valuable when a threat actor rotates domain names while reusing the same IP infrastructure. The owner name encodes the IP address in reverse-dotted notation, prefixed by the CIDR prefix length and suffixed with
.rpz-ip. The encoding format is:
[prefix-length].[octet4].[octet3].[octet2].[octet1].rpz-ip
; Block any DNS response that resolves to 10.10.5.100 (/32)
32.100.5.10.10.rpz-ip CNAME .
; Block any response within the 172.16.100.0/24 range
24.0.100.16.172.rpz-ip CNAME .
Nameserver Name Triggers (rpz-nsdname)
Triggers based on the authoritative nameserver name for the queried domain. If a domain delegates to a nameserver that is known to host malware distribution infrastructure, you can block all domains under that nameserver regardless of their individual names. The trigger suffix is
.rpz-nsdname:
; Block domains whose authoritative NS is ns1.bulletproof-hosting.bz
ns1.bulletproof-hosting.bz.rpz-nsdname CNAME .
ns2.bulletproof-hosting.bz.rpz-nsdname CNAME .
Nameserver IP Triggers (rpz-nsip)
Similar to
rpz-nsdnamebut triggers on the resolved IP address of the authoritative nameserver rather than its hostname. Useful when hostile nameservers use disposable hostnames but consistent IP infrastructure. IP encoding follows the same reversed-dotted notation as
rpz-ip:
; Block domains whose NS resolves to 10.99.1.1
32.1.1.99.10.rpz-nsip CNAME .
Client IP Triggers (rpz-client-ip)
Triggers based on the IP address of the DNS client sending the query. This enables per-host policy enforcement directly within BIND9. A quarantined workstation can be restricted from resolving external domains, or a specific device can be subject to stricter content filtering. The encoding mirrors
rpz-ipbut uses the suffix
.rpz-client-ip:
; Apply NXDOMAIN to all queries from quarantined host 10.10.1.45
32.45.1.10.10.rpz-client-ip CNAME .
Understanding RPZ Action Types
NXDOMAIN – CNAME .
Returns a Non-Existent Domain response to the client. The queried name appears not to exist. This is the most common RPZ action for blocklists. Clients receive a standard NXDOMAIN and display a domain-not-found error rather than a connection refused, which makes the blocking less obvious to users and avoids browser security warnings that might arise from a refused TCP connection.
malware-distribution.net CNAME .
NODATA – CNAME *.
Returns an empty answer section with an NOERROR rcode. The domain appears to exist but has no records of the queried type. This is useful when NXDOMAIN causes application errors in certain software stacks that treat non-existent domains as fatal and refuse to fall back gracefully:
tracker-domain.com CNAME *.
DROP – CNAME rpz-drop.
Silently discards the DNS query with no response sent to the client. The client resolver retries and eventually times out. Use this action sparingly — it introduces query latency for end users and can cause noticeable slowdowns. DROP is most appropriate for blocking aggressive DNS scanners or probing hosts where you want to deny even the NXDOMAIN information:
dns-scanner-domain.net CNAME rpz-drop.
PASSTHRU – CNAME rpz-passthru.
Explicitly allows a query to bypass all lower-priority RPZ zones and proceed to normal recursive resolution. PASSTHRU is used to whitelist specific domains that would otherwise be caught by a wildcard block or an IP-range trigger. Always configure PASSTHRU entries in a higher-priority RPZ zone than your blocklist zones so the whitelist is evaluated first:
; Allow this host to resolve update servers even if the IP range is blocked
updates.solvethenetwork.com CNAME rpz-passthru.
Local Data – Walled Garden Redirect
Return a specific IP address instead of performing recursive resolution. When a client queries a blocked domain, BIND9 returns your internal sinkhole IP directly. This powers a walled garden where a web server at the sinkhole IP serves an informational page explaining the block, providing far better user experience than a cryptic NXDOMAIN. Provide both an A record and a wildcard A to catch subdomain queries:
phishing-campaign.net A 10.10.9.99
*.phishing-campaign.net A 10.10.9.99
Building a Multi-Policy Production Zone File
A production RPZ zone combines multiple trigger types and actions into a single comprehensive policy file organized by category. The example below represents a realistic deployment for sw-infrarunbook-01:
$TTL 300
@ IN SOA sw-infrarunbook-01.solvethenetwork.com. infrarunbook-admin.solvethenetwork.com. (
2024031502
3600
900
604800
300 )
IN NS sw-infrarunbook-01.solvethenetwork.com.
; --- Category: Malware C2 Domains (NXDOMAIN) ---
cnc-botnet.xyz CNAME .
*.cnc-botnet.xyz CNAME .
exfil-server.cc CNAME .
*.exfil-server.cc CNAME .
; --- Category: Phishing (Walled Garden at 10.10.9.99) ---
login-portal-fake.com A 10.10.9.99
*.login-portal-fake.com A 10.10.9.99
creds-harvest.net A 10.10.9.99
*.creds-harvest.net A 10.10.9.99
; --- Category: Hostile IP Ranges (rpz-ip) ---
; Block responses pointing into a known hostile /24
24.0.55.203.185.rpz-ip CNAME .
; --- Category: Hostile Nameservers (rpz-nsdname) ---
ns1.bulletproof-hosting.bz.rpz-nsdname CNAME .
ns2.bulletproof-hosting.bz.rpz-nsdname CNAME .
; --- Category: Quarantine Policy (rpz-client-ip) ---
; Host 10.10.1.88 is under quarantine -- block all external resolution
32.88.1.10.10.rpz-client-ip CNAME .
; --- Whitelist: sw-infrarunbook-01 bypasses all blocks (rpz-client-ip PASSTHRU) ---
32.53.1.10.10.rpz-client-ip CNAME rpz-passthru.
Configuring RPZ Logging
RPZ policy matches are logged under the
rpzBIND9 logging category. Add or update a logging block in
/etc/bind/named.conf.optionsto capture RPZ hits to a dedicated file:
logging {
channel rpz_log {
file "/var/log/named/rpz.log" versions 7 size 20m;
severity info;
print-time yes;
print-severity yes;
print-category yes;
};
channel default_log {
file "/var/log/named/named.log" versions 5 size 10m;
severity dynamic;
print-time yes;
print-category yes;
};
category rpz { rpz_log; };
category default { default_log; };
};
Create the log directory and assign ownership to the BIND9 process user:
sudo mkdir -p /var/log/named
sudo chown bind:bind /var/log/named
With this configuration every RPZ policy match writes a structured log line to
/var/log/named/rpz.logcontaining the client IP address, queried name, matched RPZ zone name, and applied action. This log is invaluable during incident response — a spike in RPZ hits from a single internal IP often indicates a compromised host attempting C2 callback.
Validating Configuration and Reloading BIND9
Before reloading, always validate both the named configuration and the zone file to prevent service disruptions on sw-infrarunbook-01:
sudo named-checkconf
sudo named-checkzone rpz.solvethenetwork.com /etc/bind/zones/rpz.solvethenetwork.com.db
If
named-checkconfproduces no output and
named-checkzonereports OK, reload BIND9 without dropping active client connections:
sudo rndc reload
To reload only the RPZ zone after adding or removing entries — faster than a full server reload when only zone data changed:
sudo rndc reload rpz.solvethenetwork.com
Testing RPZ Behavior with dig
Direct test queries at the BIND9 resolver address and verify each action type behaves as expected. Replace
10.10.1.53with the IP on which your resolver is listening:
# Test NXDOMAIN action -- should return status: NXDOMAIN
dig @10.10.1.53 cnc-botnet.xyz A
# Test walled garden -- should return A 10.10.9.99
dig @10.10.1.53 login-portal-fake.com A
# Test wildcard entry -- subdomain should also return 10.10.9.99
dig @10.10.1.53 accounts.login-portal-fake.com A
# Test legitimate internal domain passes through normally
dig @10.10.1.53 solvethenetwork.com A
A successful NXDOMAIN RPZ block returns output similar to the following:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 28471
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1
;; ANSWER SECTION:
cnc-botnet.xyz. 300 IN CNAME .
;; AUTHORITY SECTION:
rpz.solvethenetwork.com. 300 IN SOA sw-infrarunbook-01.solvethenetwork.com. ...
Notice that BIND9 returns the CNAME record from the RPZ zone in the answer section. This is intentional — it allows downstream resolvers and security monitoring tools to identify that the response was RPZ-synthesized rather than returned by the authoritative server for the queried domain.
Using Multiple RPZ Zones with Priority Order
BIND9 evaluates RPZ zones in the order they appear in the
response-policydirective. The first match wins. Use this layering behaviour to place a local whitelist above blocklist zones, or to tier multiple threat feed zones by trust level:
response-policy {
zone "whitelist.rpz.solvethenetwork.com" policy passthru;
zone "rpz-local.solvethenetwork.com" policy given;
zone "rpz-threatfeed.solvethenetwork.com" policy given;
};
With this configuration, the whitelist zone is evaluated first — any PASSTHRU match immediately sends the query to normal recursive resolution, bypassing the local blocklist and the threat feed zone entirely. Declare each zone individually in
named.conf.local.
Integrating an External RPZ Threat Feed as a Secondary Zone
Commercial and open-source threat intelligence providers publish RPZ-formatted zones consumable via authenticated AXFR/IXFR zone transfer. Configure the feed as a secondary zone pointing at the provider's primary server. Use a TSIG key for transfer authentication:
key "rpzfeed-tsig-key" {
algorithm hmac-sha256;
secret "c2VjcmV0a2V5aGVyZWJhc2U2NGVuY29kZWQ=";
};
zone "rpz-threatfeed.solvethenetwork.com" {
type secondary;
primaries { 10.20.0.10 key rpzfeed-tsig-key; };
file "/var/cache/bind/rpz-threatfeed.solvethenetwork.com.db";
allow-query { none; };
allow-transfer { none; };
};
After the initial AXFR completes, BIND9 automatically loads the feed into the RPZ engine. Subsequent IXFR incremental updates are applied without a full reload. Monitor transfer status with:
sudo rndc zonestatus rpz-threatfeed.solvethenetwork.com
Performance Tuning for Large RPZ Zones
RPZ lookup adds a small amount of latency to every query. On modern hardware the overhead is negligible for zones with tens of thousands of entries, but large-scale deployments warrant attention to the following settings:
- TTL tuning: Short TTLs (300s) allow rapid policy propagation but increase zone transfer frequency. For stable, slowly-changing blocklists use longer TTLs (3600s or higher)
- Memory usage: BIND9 holds the RPZ zone in memory. A zone containing 500,000 entries consumes approximately 150–200 MB of RAM. Monitor named memory with
rndc stats
and review/var/cache/bind/named_stats.txt
- qname-wait-recurse: Setting
qname-wait-recurse no;
on a zone allows BIND9 to apply QNAME triggers before recursion completes, reducing blocked-domain query latency at the cost of not evaluating IP or NS triggers for those queries - break-dnssec: By default BIND9 will not override DNSSEC-validated responses with RPZ actions. If you need to block DNSSEC-signed malicious domains, add
break-dnssec yes;
to that zone entry
response-policy {
zone "rpz.solvethenetwork.com" policy given
qname-wait-recurse no
break-dnssec yes;
};Related Articles
- [DNS] Securing BIND9 Zone Transfers with TSIG Keys – Complete Production Guide
- [DNS] Configuring Dynamic DNS (DDNS) with BIND9 and nsupdate – Production Guide
- [DNS] Configuring Split-Horizon DNS with BIND9 Views on Ubuntu – Production Guide
- [DNS] How to Configure DNSSEC on BIND9 (Ubuntu) – Complete Step-by-Step Guide
