Securing DNS Traffic with DNS over TLS (DoT) on BIND

Securing DNS Traffic with DNS over TLS (DoT) on BIND

Traditional DNS queries travel in plaintext, exposing your browsing habits to network observers. DNS over TLS (DoT) encrypts DNS traffic between clients and resolvers, providing privacy and preventing tampering. This post covers implementing DoT on BIND 9 for both resolver and forwarder configurations.

Understanding DNS over TLS

How DoT Works

DoT wraps standard DNS queries in TLS encryption:

  1. Client initiates TLS connection to port 853
  2. TLS handshake establishes encrypted channel
  3. DNS queries and responses flow encrypted
  4. Connection may persist for multiple queries

Benefits of DoT

  • Privacy: ISPs and network observers cannot see DNS queries
  • Integrity: Prevents DNS query/response manipulation
  • Authentication: Server identity verified via TLS certificates
  • Compliance: Helps meet data protection requirements

DoT vs DoH

Feature DNS over TLS (DoT) DNS over HTTPS (DoH)
Port 853 (dedicated) 443 (shared with HTTPS)
Protocol TLS directly HTTP/2 over TLS
Blockable Easily (port 853) Harder to distinguish
Performance Better (simpler) Slightly more overhead
Use case Network operators End users/browsers

Prerequisites

Before configuring DoT, ensure you have:

  1. BIND 9.17+ (native DoT support) or BIND with stunnel/nginx
  2. Valid TLS certificate (Let's Encrypt works well)
  3. Port 853 accessible through firewall
  4. A working recursive resolver setup

Native DoT in BIND 9.17+

BIND 9.17 and later include native DNS over TLS support.

Basic DoT Listener Configuration

// /etc/bind/named.conf.options

options {
    directory "/var/cache/bind";
    
    // Standard DNS listener
    listen-on { 127.0.0.1; 192.168.1.1; };
    listen-on-v6 { ::1; };
    
    // DNS over TLS listener
    listen-on port 853 tls local-tls { 127.0.0.1; 192.168.1.1; };
    listen-on-v6 port 853 tls local-tls { ::1; };
    
    // Resolver settings
    recursion yes;
    allow-recursion { localhost; 192.168.0.0/16; };
    
    dnssec-validation auto;
    
    // Performance tuning for DoT
    tcp-clients 1000;
    tcp-keepalive-timeout 30;
};

// TLS configuration
tls local-tls {
    cert-file "/etc/bind/tls/cert.pem";
    key-file "/etc/bind/tls/key.pem";
    
    // Optional: Diffie-Hellman parameters for forward secrecy
    dhparam-file "/etc/bind/tls/dhparam.pem";
    
    // TLS protocol settings
    protocols { TLSv1.2; TLSv1.3; };
    
    // Strong cipher suites
    ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305";
    
    // Prefer server cipher order
    prefer-server-ciphers yes;
    
    // Session resumption
    session-tickets yes;
};

Generating TLS Certificates

Using Let's Encrypt

# Install certbot
apt install certbot

# Obtain certificate (standalone method)
certbot certonly --standalone -d dns.example.com

# Create directory for BIND
mkdir -p /etc/bind/tls

# Copy certificates with correct permissions
cp /etc/letsencrypt/live/dns.example.com/fullchain.pem /etc/bind/tls/cert.pem
cp /etc/letsencrypt/live/dns.example.com/privkey.pem /etc/bind/tls/key.pem
chown bind:bind /etc/bind/tls/*.pem
chmod 640 /etc/bind/tls/*.pem

Auto-renewal Hook

#!/bin/bash
# /etc/letsencrypt/renewal-hooks/deploy/bind-tls.sh

DOMAIN="dns.example.com"
BIND_TLS_DIR="/etc/bind/tls"

# Copy new certificates
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $BIND_TLS_DIR/cert.pem
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $BIND_TLS_DIR/key.pem

# Set permissions
chown bind:bind $BIND_TLS_DIR/*.pem
chmod 640 $BIND_TLS_DIR/*.pem

# Reload BIND
rndc reload

echo "BIND TLS certificates updated"

Self-Signed Certificate (Testing Only)

#!/bin/bash
# Generate self-signed certificate for testing

mkdir -p /etc/bind/tls
cd /etc/bind/tls

# Generate private key
openssl genrsa -out key.pem 4096

# Generate certificate signing request
openssl req -new -key key.pem -out cert.csr \
    -subj "/CN=dns.example.com/O=Example/C=US"

# Self-sign certificate (valid for 365 days)
openssl x509 -req -days 365 -in cert.csr \
    -signkey key.pem -out cert.pem

# Generate DH parameters (optional but recommended)
openssl dhparam -out dhparam.pem 2048

# Set permissions
chown bind:bind *.pem
chmod 640 *.pem

DoT Forwarding Configuration

Configure BIND to forward queries to upstream DoT servers:

// /etc/bind/named.conf.options

options {
    directory "/var/cache/bind";
    
    // Listen for queries from local network
    listen-on { 127.0.0.1; 192.168.1.1; };
    listen-on-v6 { ::1; };
    
    recursion yes;
    allow-recursion { localhost; 192.168.0.0/16; };
    
    // Forward all queries via DoT
    forward only;
    forwarders port 853 tls upstream-dot {
        1.1.1.1;          // Cloudflare
        1.0.0.1;          // Cloudflare secondary
        8.8.8.8;          // Google
        9.9.9.9;          // Quad9
    };
    
    dnssec-validation auto;
};

// TLS configuration for upstream servers
tls upstream-dot {
    // CA bundle for verifying upstream certificates
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    
    // Hostname verification (important for security)
    remote-hostname "cloudflare-dns.com";
    
    // Only use TLS 1.2 and 1.3
    protocols { TLSv1.2; TLSv1.3; };
};

Multiple Upstream Providers with Different TLS Settings

// Different TLS configs for different providers
tls cloudflare-tls {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    remote-hostname "cloudflare-dns.com";
};

tls google-tls {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    remote-hostname "dns.google";
};

tls quad9-tls {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    remote-hostname "dns.quad9.net";
};

options {
    // Primary: Cloudflare
    forwarders port 853 tls cloudflare-tls {
        1.1.1.1;
        1.0.0.1;
    };
    
    forward first;  // Fall back to root servers if forwarders fail
};

// Alternative: Split forwarding by zone
zone "example.com" {
    type forward;
    forward only;
    forwarders port 853 tls google-tls { 8.8.8.8; };
};

DoT with stunnel (For Older BIND Versions)

If running BIND < 9.17, use stunnel as a TLS wrapper:

stunnel Server Configuration

; /etc/stunnel/dns-dot.conf

; Global settings
setuid = stunnel4
setgid = stunnel4
pid = /var/run/stunnel4/dns-dot.pid

; Logging
output = /var/log/stunnel4/dns-dot.log
debug = notice

; DoT server - accepts TLS connections, forwards to local BIND
[dns-dot-server]
accept = 853
connect = 127.0.0.1:53
cert = /etc/stunnel/cert.pem
key = /etc/stunnel/key.pem

; TLS options
options = NO_SSLv2
options = NO_SSLv3
options = SINGLE_DH_USE
options = SINGLE_ECDH_USE
ciphers = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384

; Protocol version
sslVersion = TLSv1.2

stunnel Client Configuration (DoT Forwarding)

; /etc/stunnel/dns-dot-client.conf

[dns-dot-cloudflare]
; Local plain DNS listener
accept = 127.0.0.2:53
; Remote DoT server
connect = 1.1.1.1:853
client = yes

; Verify server certificate
verifyChain = yes
CAfile = /etc/ssl/certs/ca-certificates.crt
checkHost = cloudflare-dns.com

[dns-dot-google]
accept = 127.0.0.3:53
connect = 8.8.8.8:853
client = yes
verifyChain = yes
CAfile = /etc/ssl/certs/ca-certificates.crt
checkHost = dns.google

Then configure BIND to forward to stunnel:

// Forward to local stunnel proxies
options {
    forwarders {
        127.0.0.2;  // stunnel -> Cloudflare DoT
        127.0.0.3;  // stunnel -> Google DoT
    };
    forward only;
};

Performance Optimization

Connection Pooling

options {
    // Keep TCP/TLS connections open longer
    tcp-keepalive-timeout 60;
    
    // Allow more concurrent TCP/TLS connections
    tcp-clients 2000;
    
    // Enable TCP fast open if supported
    // (kernel must support it: sysctl net.ipv4.tcp_fastopen=3)
};

Query Pipelining

Modern DoT clients can send multiple queries over a single TLS connection:

tls local-tls {
    cert-file "/etc/bind/tls/cert.pem";
    key-file "/etc/bind/tls/key.pem";
    
    // Enable session resumption for faster reconnects
    session-tickets yes;
};

Monitoring DoT Performance

#!/bin/bash
# Test DoT response time

DOT_SERVER="dns.example.com"

# Test with kdig (knot-dnsutils)
kdig +tls @$DOT_SERVER example.com A

# Measure TLS handshake time
echo | openssl s_client -connect $DOT_SERVER:853 2>&1 | \
    grep -E "(Verify return|Protocol|Cipher)"

# Test with dog (modern dig alternative)
dog --tls @$DOT_SERVER example.com A

Firewall Configuration

iptables Rules

#!/bin/bash
# Allow DoT traffic

# Allow incoming DoT (port 853)
iptables -A INPUT -p tcp --dport 853 -j ACCEPT

# Allow outgoing DoT to upstream resolvers
iptables -A OUTPUT -p tcp --dport 853 -j ACCEPT

# Rate limiting to prevent abuse
iptables -A INPUT -p tcp --dport 853 -m state --state NEW \
    -m recent --set --name DOT
iptables -A INPUT -p tcp --dport 853 -m state --state NEW \
    -m recent --update --seconds 60 --hitcount 100 --name DOT -j DROP

nftables Rules

#!/usr/sbin/nft -f

table inet filter {
    chain input {
        # Allow DoT
        tcp dport 853 accept
        
        # Rate limit DoT connections
        tcp dport 853 meter dot-meter { ip saddr limit rate over 100/minute } drop
    }
    
    chain output {
        # Allow outgoing DoT
        tcp dport 853 accept
    }
}

Client Configuration

systemd-resolved

# /etc/systemd/resolved.conf
[Resolve]
DNS=192.168.1.1#dns.example.com
DNSOverTLS=yes
DNSSEC=yes

Android Private DNS

  1. Settings -> Network & Internet -> Advanced -> Private DNS
  2. Enter: dns.example.com

Stubby (General-Purpose DoT Client)

# /etc/stubby/stubby.yml
resolution_type: GETDNS_RESOLUTION_STUB
dns_transport_list:
  - GETDNS_TRANSPORT_TLS
tls_authentication: GETDNS_AUTHENTICATION_REQUIRED
tls_query_padding_blocksize: 128
edns_client_subnet_private: 1
idle_timeout: 10000
listen_addresses:
  - 127.0.0.1@53000
upstream_recursive_servers:
  - address_data: 192.168.1.1
    tls_auth_name: "dns.example.com"

Complete Production Configuration

Here's a complete DoT-enabled resolver configuration:

// /etc/bind/named.conf

acl trusted {
    localhost;
    192.168.0.0/16;
    10.0.0.0/8;
};

options {
    directory "/var/cache/bind";
    
    // Standard DNS
    listen-on { 127.0.0.1; 192.168.1.1; };
    listen-on-v6 { ::1; };
    
    // DNS over TLS
    listen-on port 853 tls dot-server { 127.0.0.1; 192.168.1.1; };
    listen-on-v6 port 853 tls dot-server { ::1; };
    
    // Access control
    allow-query { trusted; };
    allow-recursion { trusted; };
    recursion yes;
    
    // DNSSEC
    dnssec-validation auto;
    
    // Performance
    tcp-clients 2000;
    tcp-keepalive-timeout 60;
    
    // Security
    version none;
    hostname none;
    
    // Forward to upstream DoT (optional)
    // forwarders port 853 tls upstream-dot { 1.1.1.1; 8.8.8.8; };
    // forward first;
};

// Server TLS configuration
tls dot-server {
    cert-file "/etc/bind/tls/fullchain.pem";
    key-file "/etc/bind/tls/privkey.pem";
    dhparam-file "/etc/bind/tls/dhparam.pem";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305";
    prefer-server-ciphers yes;
    session-tickets yes;
};

// Upstream DoT configuration (for forwarding)
tls upstream-dot {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    protocols { TLSv1.2; TLSv1.3; };
};

logging {
    channel tls_log {
        file "/var/log/named/tls.log" versions 5 size 10m;
        severity info;
        print-time yes;
        print-category yes;
    };
    category network { tls_log; };
};

Troubleshooting

Test DoT Connectivity

# Test with kdig
kdig +tls @dns.example.com example.com

# Test with openssl
echo -e "\x00\x1e\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01" | \
    openssl s_client -connect dns.example.com:853 -quiet

# Verbose TLS handshake
openssl s_client -connect dns.example.com:853 -status -tlsextdebug

Common Issues

Certificate errors:

# Check certificate validity
openssl x509 -in /etc/bind/tls/cert.pem -text -noout | grep -A2 Validity

# Verify certificate chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/bind/tls/cert.pem

Permission errors:

# Ensure BIND can read certificates
ls -la /etc/bind/tls/
chown bind:bind /etc/bind/tls/*.pem
chmod 640 /etc/bind/tls/*.pem

Connection refused:

# Check if BIND is listening on 853
ss -tlnp | grep 853

# Check firewall
iptables -L INPUT -n | grep 853

Security Best Practices

  1. Use valid certificates - Self-signed certificates don't provide authentication
  2. Enable DNSSEC - DoT encrypts transport, DNSSEC authenticates content
  3. Restrict TLS versions - Only allow TLS 1.2 and 1.3
  4. Monitor certificate expiry - Set up alerts before certificates expire
  5. Use strong cipher suites - Prefer ECDHE for forward secrecy
  6. Rate limit connections - Prevent DoS attacks on port 853
  7. Log TLS events - Monitor for connection failures and attacks

Conclusion

DNS over TLS provides essential privacy and security for DNS traffic. With BIND 9.17+, native DoT support makes deployment straightforward. For older versions, stunnel provides a reliable wrapper solution.

Key takeaways:

  • Use valid TLS certificates from a trusted CA
  • Enable both server-side DoT (for clients) and client-side DoT (for upstream forwarding)
  • Configure proper TLS settings for security and performance
  • Monitor and maintain certificate renewals

The next post will cover DNS over HTTPS (DoH), which provides similar encryption using HTTPS and is increasingly supported by browsers and applications.

Read more

HAProxy Monitoring with Prometheus: Complete Observability Guide

HAProxy Monitoring with Prometheus: Complete Observability Guide

Monitoring HAProxy is essential for maintaining reliable load balancing infrastructure. Prometheus provides powerful metrics collection, alerting capabilities, and seamless Grafana integration for visualizing HAProxy performance and health. Why Prometheus for HAProxy? Prometheus offers: * Pull-based metrics - Prometheus scrapes HAProxy metrics endpoints * Time-series database - Store historical data for trend analysis

By Patrick de Ruiter