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:
- Client initiates TLS connection to port 853
- TLS handshake establishes encrypted channel
- DNS queries and responses flow encrypted
- 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:
- BIND 9.17+ (native DoT support) or BIND with stunnel/nginx
- Valid TLS certificate (Let's Encrypt works well)
- Port 853 accessible through firewall
- 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
- Settings -> Network & Internet -> Advanced -> Private DNS
- 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
- Use valid certificates - Self-signed certificates don't provide authentication
- Enable DNSSEC - DoT encrypts transport, DNSSEC authenticates content
- Restrict TLS versions - Only allow TLS 1.2 and 1.3
- Monitor certificate expiry - Set up alerts before certificates expire
- Use strong cipher suites - Prefer ECDHE for forward secrecy
- Rate limit connections - Prevent DoS attacks on port 853
- 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.