In the first part, I described how I set up the basic OpenBSD WireGuard VPN server. I also hinted that I wanted to set up my own validating, filtering DNS server. With a little bit of spare time during the holidays I decided now was a good time as any.
Making sure the VPN server uses the local Unbound DNS resolver first
Before I set up Unbound itself, I need to make sure that the VPN server’s resolv.conf points at its local DNS server first.
My Vultr VPS has a static IP, which it receives via DHCP. It also receives information about its upstream DNS servers via DHCP. So as the first step, we need to update /etc/dhclient.conf
to ensure that resolv.conf lists our local server first. We do this by adding the following line to /etc/dhclient.conf:
prepend domain-name-servers 127.0.0.1;
In fact, that is the complete dhclient.conf file on my VPN server.
Setting up Unbound as a validating DNS server
OpenBSD already includes the Unbound authoritative DNS server, so it’s a matter of configuring it to act as the local DNS server and also to enable validation. Everything you need to run the server is already on the system.
Root server list and trust anchor files
Once we told the local system to use the local Unbound as its DNS server, we need to configure it. Note that dhclient will still add the upstream provider DNS server to resolv.conf, so it is still there as a fallback.
Time to set up Unbound. A validating DNS server needs a few more pieces of data compared to a standard recursive DNS server. The two important items are the trust anchor and the root hints so Unbound knows where to find the root name servers. Note that OpenBSD runs Unbound in a sandbox and thus all the configuration is under /var/unbound
.
We start with retrieving the root.hints file that contains the list of root DNS servers:
wget https://www.internic.net/domain/named.root -O /var/unbound/etc/root.hints
A quick check with more /var/unbound/etc/root.hints
shows that we received the expected list of root servers.
The second part we need is the trust anchor, which is the cryptographic key our DNS server needs so it can validate DNSSEC. After a couple of false starts I simply ended up using unbound-anchor to download the trust anchor and configured Unbound via the auto-trust-anchor setting to automatically check and update the trust anchor when necessary.
Pretty much all of the Internet tutorials suggest that the trust anchor should be stored in /var/unbound/etc. In my OpenBSD setup, unbound can’t write to that directory so I ended up sotring it in /var/unbound/db instead.
Unbound.conf minimal setup for validating server
Now we’re finally ready to configure unbound itself. My basic, stripped down version of the configuration looks like this:
server:
verbosity: 1
#
# Interfaces and access control. Make sure we're not suddenly
# running an open public resolver
#
outgoing-interface: 'real' server ip'
interface: 127.0.0.1
interface: 192.168.1.1 # Internal VPN interface
access-control: 127.0.0.0/8 allow
access-control: 192.168.1.0/24 allow
#
# Basic configuration
#
port: 53
do-ip4: yes
do-ip6: no # VPN server currently doesn't support IPV6
do-udp: yes
do-tcp: yes
#
# Root servers, trust anchor and validation logging
#
root-hints: "/var/unbound/etc/root.hints"
auto-trust-anchor-file: "/var/unbound/db/root.key"
val-log-level: 2
val-clean-additional: yes
#
# Some basic hardening
#
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
unwanted-reply-threshold: 10000
#
# Ensure we don't return private addresses to prevent DNS
# rebinding attacks
#
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
#
# Other settings. I don't expect to see a lot of DNS traffic so
# I turned off prefetching
#
prefetch: no
#
num-threads: 1
Now that we have a working configuration file, we need to enable and start Unbound:
vpn# rcctl enable unbound vpn# rcctl start unbound unbound(ok)
Testing DNSSEC validation
At this point we have a working server with supposedly working DNSSEC validation. Obviously we work on ’trust, but verify’. To check that we have indeed a working validating server, we can run the following command:
dig www.nic.cz. +dnssec
The header section of the result should look like this:
; <<>> DiG 9.4.2-P2 <<>> www.nic.cz. +dnssec ;; global options: printcmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18417 ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
See the bolded ‘ad’ in the flags line? Now compare this to the output of the same command, but run on my MacBook using the ISP’s resolver:
; <<>> DiG 9.10.6 <<>> www.nic.cz. +dnssec ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12527 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
The ISP’s resolver doesn’t support DNSSEC in this case, so you can see the ‘ad’ flag missing. That flag indicates that the result from the upstream server validated.
Update the WireGuard config on the client
After I verified that the validation is working, it is time to update the WireGuard configuration on all clients so they point at our shiny new validating server rather than the third party DNS servers I used in the first installment.
Summary
At this point I have a validating DNS server, however it still passes through requests for every domain including the typical ad slinging domains. In the next installment of this series I’ll add filtering rules to try to get rid of most, if not all of them.
The third installment on how to add filtering to the Unbound DNS setup can be found here.