DNS Over TLS Using BIND And Nginx
Test Run
Deploying a Simple DoT-DNS Gateway
The NGINX Stream (TCP/UDP) module supports SSL termination, and so it’s actually really simple to set up a DoT service. You can create a simple DoT gateway in just a few lines of NGINX configuration. You need an upstream
block for your DNS servers - I am using the default Google DNS 8.8.8.8
for now - and a server block for TLS termination.
I am using a modified version of the Docker NGINX Ingress that already has a self-signed certificate I can use:
nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 15000;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 2048;
multi_accept on;
use epoll;
}
stream {
# DNS upstream pool
upstream dns {
zone dns 64k;
server 8.8.8.8:53;
}
# DoT server for decryption
server {
listen 853 ssl;
# ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
# ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
proxy_pass dns;
include self-signed.conf;
include ssl-params.conf;
}
}
self-signed.conf
ssl_certificate /etc/nginx/conf.d/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/conf.d/ssl/private/nginx-selfsigned.key;
ssl-params.conf
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/conf.d/ssl/private/dhparam.pem;
ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;
ssl_conf_command Options PrioritizeChaCha;
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
Gateway Container
Start the docker container with:
docker network create --subnet=172.24.0.0/16 instar-net
docker pull nginx:alpine
docker run -d \
--rm \
--name=gateway \
--net=instar-net \
--ip=172.24.0.16 \
-p 853:853 \
-v /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf \
-v /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf \
-v /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl \
nginx:alpine
Testing
Verify that the TLS service is available - the IPv4 address 16.132.12.32
, in the following command is the IP address of my Docker host server that is running the gateway:
openssl s_client -host 16.132.12.32 -port 853 -showcerts
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = 16.132.12.32
verify error:num=18:self signed certificate
verify return:1
depth=0 C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = 16.132.12.32
verify return:1
...
I am going to use the getDNS Utilities to test the Gateway:
apt install getdns-utils
First I try a run without TLS - which cannot work. The IPv4 address 16.132.12.32
, in the following command is the IP address of my Docker host server that is running the gateway:
getdns_query @16.132.12.32 -s -a -A www.instar.com
{
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for www.instar.com.>,
"just_address_answers": [],
"replies_full": [],
"replies_tree": [],
"status": GETDNS_RESPSTATUS_ALL_TIMEOUT
}
An error occurred: The callback got a callback_type of 702. Exiting.
Error : 'The requested action timed out; response is filled in with empty structures'
And now to TLS:
getdns_query @16.132.12.32 -s -a -A -l L www.instar.com
{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for www.instar.com.>,
"just_address_answers":
[
{
"address_data": <bindata for 49.12.0.118>,
"address_type": <bindata of "IPv4">
}
],
...
"status": GETDNS_RESPSTATUS_GOOD
}
The IP address 49.12.0.118
is successfully resolved for www.instar.com
.
Bind9 in Docker
I am going to use my Bind9 Docker Container and run it inside the same Docker network instar-net
:
docker network create --subnet=172.24.0.0/16 instar-net
docker run -d --rm --name=ddns-master --net=instar-net --ip=172.24.0.2 ddns-master
Gateway Configuration
Now I can replace the 8.8.8.8
Google DNS Server and replace it with the Bind9 Docker container IP 172.24.0.2
or the container name ddns-master
. I am also adding a stream for port 53
so I don't have to expose any ports from the Bind9 container:
user nginx;
worker_processes auto;
worker_rlimit_nofile 15000;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 2048;
multi_accept on;
use epoll;
}
stream {
# DNS Upstream
upstream dns {
zone dns 64k;
server ddns-master:53;
}
# DNS Ingress
server {
listen 53 udp reuseport;
proxy_timeout 20s;
proxy_pass dns;
}
# DoT Ingress TLS Endpoint
server {
listen 853 ssl;
# ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
# ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
proxy_timeout 20s;
proxy_pass dns;
include self-signed.conf;
include ssl-params.conf;
}
}
docker run -d \
--rm \
--name=gateway \
--net=instar-net \
--ip=172.24.0.16 \
-p 853:853/tcp \
-p 53:53/udp \
-v /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf \
-v /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf \
-v /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl \
nginx:alpine
Testing
Without TLS:
getdns_query @16.132.12.32 -s -a -A service2.instar-net.io
{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for service2.instar-net.io.>,
"just_address_answers":
[
{
"address_data": <bindata for 172.24.0.4>,
"address_type": <bindata of "IPv4">
}
],
...
"status": GETDNS_RESPSTATUS_GOOD
}
But I am seeing an error message when switching to TLS:
getdns_query @16.132.12.32 -s -a -A -l L service2.instar-net.io
{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for service2.instar-net.io.>,
"just_address_answers": [],
...
"status": GETDNS_RESPSTATUS_NO_NAME
}
An error occurred: The callback got a callback_type of 702. Exiting.
Error : 'The requested action timed out; response is filled in with empty structures'
DNSUtils
I am able to resolve the IP of my service as defined in Bind9 using nslookup
:
apt-get install dnsutils
nslookup service2.instar-net.io 16.132.12.32
Server: 16.132.12.32
Address: 16.132.12.32#53
Name: service2.instar-net.io
Address: 172.24.0.4
But there is no way to check the TLS connecting through the gateway proxy.
Knot DNSUtils
That is where kdig comes in:
apt-get install knot-dnsutils
Without TLS:
kdig service1.instar-net.io -t A @16.132.12.32
;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 17361
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1
;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 16017
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 0
;; QUESTION SECTION:
;; service2.instar-net.io. IN A
;; ANSWER SECTION:
service2.instar-net.io. 86400 IN A 172.24.0.4
;; Received 56 B
;; Time 2022-03-10 08:31:07 CET
;; From 1.2.3.4@53(UDP) in 0.0 ms
And here it also seems to be working over TLS - I am getting the 172.24.0.3
resolution for Service1, as expected:
kdig +tcp +tls -p 853 service1.instar-net.io -t A @16.132.12.32
;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 57694
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1
;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR
;; QUESTION SECTION:
;; service1.instar-net.io. IN A
;; ANSWER SECTION:
service1.instar-net.io. 86400 IN A 172.24.0.3
;; Received 67 B
;; Time 2022-03-09 13:33:15 CET
;; From 16.132.12.32@853(TCP) in 1.4 ms
And the forwarder is working as well:
kdig +tcp +tls -p 853 www.instar.com -t A @16.132.12.32
;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 22289
;; Flags: qr rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1
;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR
;; QUESTION SECTION:
;; www.instar.com. IN A
;; ANSWER SECTION:
www.instar.com. 600 IN A 49.12.0.118
;; Received 59 B
;; Time 2022-03-10 04:34:23 CET
;; From 16.132.12.32@853(TCP) in 41.4 ms
Docker-Compose
Wrapping everything up into a compose file:
version: "3.9"
services:
bind:
build:
context: .
dockerfile: Dockerfile
image: dns-master:latest
networks:
bindnet:
ipv4_address: 172.24.0.2
gateway:
image: nginx:alpine
ports:
- "53:53/udp"
- "853:853/tcp"
networks:
bindnet:
ipv4_address: 172.24.0.16
volumes:
- /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf
- /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf
- /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf
- /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl
networks:
bindnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.24.0.0/16
To execute the compose file first run docker-compose build
to build the Bind9 Docker container - the Dockerfile and all dependencies have to be in the same directory as the compose file:
FROM internetsystemsconsortium/bind9:9.18
RUN apt update \
&& apt install -y \
bind9-doc \
dnsutils \
geoip-bin \
mariadb-server \
net-tools
# Copy configuration files
COPY configuration/named.conf.options /etc/bind/
COPY configuration/named.conf.local /etc/bind/
COPY configuration/db.instar-net.io /etc/bind/zones/
# Expose Ports
EXPOSE 53/tcp
EXPOSE 53/udp
EXPOSE 953/tcp
# Start the Name Service
CMD ["/usr/sbin/named", "-g", "-c", "/etc/bind/named.conf", "-u", "bind"]