BIND RPZ Content Filtering

DNS-based content filtering using BIND Response Policy Zones (RPZ). Block YouTube, social media, or any domain at the DNS layer.

Overview

Item Value

Primary DNS

bind-01 (10.50.1.90)

Secondary DNS

bind-02 (10.50.1.91)

RPZ Zone File

/var/named/rpz.zone

Effectiveness

~70-80% (bypassed by DoH/VPN)

Time to Deploy

~1 hour

What is RPZ?

Response Policy Zones (RPZ) let you override DNS responses for specific domains. When a client queries youtube.com, BIND returns NXDOMAIN (domain doesn’t exist) instead of the real IP.

Normal DNS:
  Client → "youtube.com?" → BIND → "142.250.x.x"
  Client connects to YouTube ✓

With RPZ:
  Client → "youtube.com?" → BIND → "NXDOMAIN"
  Client gets "site not found" ✗

Limitations:

  • DNS-over-HTTPS (DoH) bypasses this - browser queries Cloudflare/Google directly

  • VPNs bypass this - DNS goes through VPN tunnel

  • Hardcoded IPs bypass this - apps that don’t use DNS

Mitigations (Phase 2):

  • Block DoH providers at VyOS firewall

  • Force all DNS through BIND (redirect port 53)


Phase 1: Create RPZ Zone on bind-01

1.0 Pre-flight

# Load credentials and sign SSH cert
ds d000 dev/vault && vault-ssh-sign
# SSH to bind-01
ssh {bind-name}

1.1 Verify Current State

# Check named is running
systemctl is-active named
# Check no existing RPZ config
sudo grep -n "response-policy" /etc/named.conf || echo "No RPZ configured (expected)"
# Check zone directory
ls -la {bind-zone-dir}/

1.2 Create RPZ Zone File

# Create the RPZ zone file
sudo tee /var/named/rpz.zone << 'EOF'
$TTL 300
@   IN  SOA  bind-01.inside.domusdigitalis.dev. admin.inside.domusdigitalis.dev. (
        2026031601  ; Serial (YYYYMMDDNN)
        3600        ; Refresh (1 hour)
        600         ; Retry (10 min)
        86400       ; Expire (1 day)
        300         ; Minimum TTL (5 min)
    )
    IN  NS   bind-01.inside.domusdigitalis.dev.

; ============================================================
; BLOCKED DOMAINS
; Action: CNAME . (returns NXDOMAIN)
; ============================================================

; --- YouTube ---
youtube.com             CNAME   .
*.youtube.com           CNAME   .
youtubei.googleapis.com CNAME   .
youtube-nocookie.com    CNAME   .
*.youtube-nocookie.com  CNAME   .
youtu.be                CNAME   .
yt3.ggpht.com           CNAME   .
*.yt3.ggpht.com         CNAME   .
ytimg.com               CNAME   .
*.ytimg.com             CNAME   .
googlevideo.com         CNAME   .
*.googlevideo.com       CNAME   .

; --- Add more domains below ---
; tiktok.com            CNAME   .
; *.tiktok.com          CNAME   .
; instagram.com         CNAME   .
; *.instagram.com       CNAME   .

EOF

1.3 Set Permissions

sudo chown named:named /var/named/rpz.zone
sudo chmod 640 /var/named/rpz.zone

1.4 Verify Zone File Syntax

sudo named-checkzone rpz /var/named/rpz.zone
Expected output
zone rpz/IN: loaded serial 2026031601
OK

Phase 2: Configure named.conf

2.1 Backup Current Config

sudo cp /etc/named.conf /etc/named.conf.pre-rpz-$(date +%Y%m%d)

2.2 Add RPZ Zone Definition

Add this zone block BEFORE the closing of the options section. Find the right location:

# Find where to add (after the last zone block)
sudo grep -n "^zone\|^};" /etc/named.conf | tail -10
# Add RPZ zone definition (add after last zone block)
sudo tee -a /etc/named.conf << 'EOF'

// ===========================================
// Response Policy Zone (Content Filtering)
// ===========================================
zone "rpz" IN {
    type master;
    file "rpz.zone";
    allow-query { none; };           // RPZ is internal only
    allow-transfer { 10.50.1.91; }; // Replicate to bind-02
    also-notify { 10.50.1.91; };    // Notify on updates
};
EOF

2.3 Add response-policy Statement

The response-policy statement goes inside the options { } block. Find the right location:

# Find the options block closing brace
sudo awk '/^options/,/^\};/' /etc/named.conf | tail -5

You need to add response-policy { zone "rpz"; }; BEFORE the closing }; of the options block.

# Find line number of options closing brace
LINE=$(sudo grep -n "^};" /etc/named.conf | head -1 | cut -d: -f1)
echo "Options block ends at line: $LINE"
# Insert response-policy before that line
sudo sed -i "${LINE}i\\    // RPZ Content Filtering\\n    response-policy { zone \"rpz\"; };" /etc/named.conf

2.4 Verify Configuration

# Check syntax
sudo named-checkconf /etc/named.conf

If no output, syntax is valid. If errors, fix before proceeding.

# Verify response-policy is in options block
sudo awk '/^options/,/^\};/' /etc/named.conf | grep -A1 "response-policy"
Expected output
    // RPZ Content Filtering
    response-policy { zone "rpz"; };
# Verify RPZ zone is defined
sudo grep -A5 'zone "rpz"' /etc/named.conf

Phase 3: Restart and Test on bind-01

3.1 Restart BIND

sudo systemctl restart named
# Verify running
systemctl is-active named
# Check for errors
sudo journalctl -u named --since "1 minute ago" --no-pager | grep -iE "error|fail|rpz"

3.2 Test RPZ is Loaded

# Check RPZ zone is loaded
sudo rndc zonestatus rpz
Expected output (key lines)
name: rpz
type: master
serial: 2026031601

3.3 Test Blocking

# Test YouTube (should return NXDOMAIN)
dig @localhost youtube.com +short
Expected output
(empty - NXDOMAIN)
# Verify NXDOMAIN status
dig @localhost youtube.com | grep -E "status:|ANSWER"
Expected output
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 12345
;; ANSWER SECTION:
youtube.com.            300     IN      CNAME   .
# Test non-blocked domain still works
dig @localhost google.com +short
Expected output
142.250.x.x  (actual IP)

Phase 4: Replicate to bind-02

4.1 SSH to bind-02

# From workstation (new terminal)
ssh {bind-02-name}

4.2 Add RPZ Zone as Secondary

# Backup config
sudo cp /etc/named.conf /etc/named.conf.pre-rpz-$(date +%Y%m%d)
# Add RPZ zone (secondary/slave)
sudo tee -a /etc/named.conf << 'EOF'

// ===========================================
// Response Policy Zone (Content Filtering)
// Replicated from bind-01
// ===========================================
zone "rpz" IN {
    type slave;
    file "slaves/rpz.zone";
    masters { 10.50.1.90; };
    allow-query { none; };
};
EOF

4.3 Add response-policy to options

Same process as bind-01 - find the options closing brace and insert before it:

LINE=$(sudo grep -n "^};" /etc/named.conf | head -1 | cut -d: -f1)
sudo sed -i "${LINE}i\\    // RPZ Content Filtering\\n    response-policy { zone \"rpz\"; };" /etc/named.conf

4.4 Verify and Restart

sudo named-checkconf /etc/named.conf
sudo systemctl restart named
# Check zone transfer succeeded
sudo rndc zonestatus rpz
Expected output
name: rpz
type: slave
serial: 2026031601

4.5 Test Blocking on bind-02

dig @localhost youtube.com +short
# Expected: empty (NXDOMAIN)
dig @localhost google.com +short
# Expected: IP address

Phase 5: End-to-End Validation

From your workstation (uses VyOS DHCP → BIND):

# Test YouTube blocked
dig youtube.com +short
# Expected: empty
# Test in browser
curl -I https://youtube.com 2>&1 | head -5
# Expected: "Could not resolve host"
# Verify using both DNS servers
dig @10.50.1.90 youtube.com +short
dig @10.50.1.91 youtube.com +short
# Both should return empty (NXDOMAIN)

Operations: Adding/Removing Domains

Add a Domain to Block

# SSH to bind-01
ssh {bind-name}
# Add domain (example: TikTok)
sudo tee -a /var/named/rpz.zone << 'EOF'

; --- TikTok ---
tiktok.com              CNAME   .
*.tiktok.com            CNAME   .
tiktokv.com             CNAME   .
*.tiktokv.com           CNAME   .
EOF
# Increment serial (CRITICAL - or zone transfer won't happen)
# Find current serial and increment
CURRENT=$(sudo grep -oP '\d{10}' /var/named/rpz.zone | head -1)
NEW=$((CURRENT + 1))
sudo sed -i "s/$CURRENT/$NEW/" /var/named/rpz.zone
echo "Serial: $CURRENT → $NEW"
# Reload zone
sudo rndc reload rpz
# Verify
dig @localhost tiktok.com +short
# Expected: empty

Remove a Domain (Unblock)

# Comment out the domain in rpz.zone
sudo sed -i 's/^youtube.com/; youtube.com/' /var/named/rpz.zone
sudo sed -i 's/^\*.youtube.com/; *.youtube.com/' /var/named/rpz.zone
# Increment serial
CURRENT=$(sudo grep -oP '\d{10}' /var/named/rpz.zone | head -1)
NEW=$((CURRENT + 1))
sudo sed -i "s/$CURRENT/$NEW/" /var/named/rpz.zone
sudo rndc reload rpz

Phase 6: Block DoH Bypass (Optional)

DNS-over-HTTPS (DoH) bypasses RPZ. Block common DoH providers at VyOS:

# SSH to vyos-01
ssh vyos-01
configure

# Block Cloudflare DoH
set firewall ipv4 name WAN_LOCAL rule 100 description "Block Cloudflare DoH"
set firewall ipv4 name WAN_LOCAL rule 100 action drop
set firewall ipv4 name WAN_LOCAL rule 100 destination address 1.1.1.1
set firewall ipv4 name WAN_LOCAL rule 100 destination port 443
set firewall ipv4 name WAN_LOCAL rule 100 protocol tcp

set firewall ipv4 name WAN_LOCAL rule 101 description "Block Cloudflare DoH IPv6"
set firewall ipv4 name WAN_LOCAL rule 101 action drop
set firewall ipv4 name WAN_LOCAL rule 101 destination address 1.0.0.1
set firewall ipv4 name WAN_LOCAL rule 101 destination port 443
set firewall ipv4 name WAN_LOCAL rule 101 protocol tcp

# Block Google DoH
set firewall ipv4 name WAN_LOCAL rule 102 description "Block Google DoH"
set firewall ipv4 name WAN_LOCAL rule 102 action drop
set firewall ipv4 name WAN_LOCAL rule 102 destination address 8.8.8.8
set firewall ipv4 name WAN_LOCAL rule 102 destination port 443
set firewall ipv4 name WAN_LOCAL rule 102 protocol tcp

set firewall ipv4 name WAN_LOCAL rule 103 description "Block Google DoH secondary"
set firewall ipv4 name WAN_LOCAL rule 103 action drop
set firewall ipv4 name WAN_LOCAL rule 103 destination address 8.8.4.4
set firewall ipv4 name WAN_LOCAL rule 103 destination port 443
set firewall ipv4 name WAN_LOCAL rule 103 protocol tcp

commit
save
exit

Troubleshooting

Symptom Cause Fix

named-checkconf fails

Syntax error in named.conf

Check error message, fix syntax

RPZ zone not loading

Zone file syntax error

named-checkzone rpz /var/named/rpz.zone

Domain still resolves

RPZ not in response-policy

Verify response-policy { zone "rpz"; }; in options block

bind-02 not getting updates

Serial not incremented

Increment serial, rndc reload rpz

Zone transfer failed

Firewall blocking 53/tcp

Check VyOS rules for bind-01 → bind-02

Check RPZ Statistics

# View RPZ hit statistics
sudo rndc stats
grep "RPZ" /var/named/data/named_stats.txt

View RPZ Query Logs

# Enable query logging (temporary)
sudo rndc querylog on

# Watch for RPZ actions
sudo tail -f /var/named/data/named.run | grep -i rpz

# Disable when done
sudo rndc querylog off

Quick Reference

Task Command

Check RPZ status

sudo rndc zonestatus rpz

Reload after changes

sudo rndc reload rpz

Test blocking

dig @localhost youtube.com +short

View zone file

sudo cat /var/named/rpz.zone

Check logs

sudo journalctl -u named -f


Common Block Lists

Social Media

facebook.com            CNAME   .
*.facebook.com          CNAME   .
instagram.com           CNAME   .
*.instagram.com         CNAME   .
tiktok.com              CNAME   .
*.tiktok.com            CNAME   .
twitter.com             CNAME   .
*.twitter.com           CNAME   .
x.com                   CNAME   .
*.x.com                 CNAME   .
snapchat.com            CNAME   .
*.snapchat.com          CNAME   .

Gaming

steampowered.com        CNAME   .
*.steampowered.com      CNAME   .
epicgames.com           CNAME   .
*.epicgames.com         CNAME   .
roblox.com              CNAME   .
*.roblox.com            CNAME   .

Streaming

netflix.com             CNAME   .
*.netflix.com           CNAME   .
hulu.com                CNAME   .
*.hulu.com              CNAME   .
disneyplus.com          CNAME   .
*.disneyplus.com        CNAME   .
twitch.tv               CNAME   .
*.twitch.tv             CNAME   .

Copy desired blocks into /var/named/rpz.zone, increment serial, reload.