Tutorial Configure encrypted DNS on Linux including DNSSEC

CaffeineAddict

Well-Known Member
Joined
Jan 21, 2024
Messages
4,004
Reaction score
4,252
Credits
32,768
What is encrypted DNS?
Purpose of encrypted DNS is to prevent eavesdropping on your DNS queries.

For web browsing there is HTTPS that prevents spying on content you send/receive from a web server, however this doesn't cover your DNS queries;
ISP, MITM, governments and similar actors can spy on your web browsing activity.

There are 2 methods of encrypted DNS:
1. DoH (DNS over HTTPS)
2. DoT (DNS over TLS)

In this tutorial we'll use DoT.

What is DNSSEC?
Purpose of DNSSEC is integrity and authenticity, it exists to confirm we're receiving DNS responses from true server (the one we configured) instead of a fake one (e.g. one set by an attacker).
Therefore its utility is to protect against attacks like DNS spoofing and cache poisoning.

In conclusion you want both DNS encryption as well as DNS authentication, this are 2 separate things.

To enable encrypted DNS there are 2 conditions:
1. DoT or DoH capable DNS resolver
2. DoT or DoH capable DNS server

Same applies to DNSSEC, both the resolver and server need to support it.

In this tutorial for DNS resolver we'll use unbound, it supports both.
unbound is FOSS,
github:
docs:

And for DNS server that supports both we'll use 2 Swiss servers (feel free to find some others but make sure they support both DoT and DNSSEC and ofc. that you trust them):
  • dns.quad9.net
  • dns.digitale-gesellschaft.ch
Links to both are here:

Installation

Package name for unbound probably depends on your distro:
Debian and derivatives:
Bash:
sudo apt install unbound
Redhat and derivatives:
Bash:
sudo dnf install unbound
Arch and derivatives:
Bash:
sudo pacman -Syu unbound

Configuration

Below is documented config file to use, it includes the most useful options to configure some of which are set to default value,
it requires changing a few lines to match your system, feel free to customize other options later.

The file should be named unbound.conf and put into /etc/unbound/unbound.conf.d

The following options should be modified:
interface: 192.168.4.100
outgoing-interface: 192.168.4.100
Set this to your NIC private IP, you can learn the IP with ip a command. (use NIC IP used to connect to internet)

root-hints: "/usr/share/dns/root.hints"
Verify that /usr/share/dns/root.hints exists.
See code commets for more info.

tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
Verify that /etc/ssl/certs/ca-certificates.crt exists.
See code commets for more info.

auto-trust-anchor-file: "/var/lib/unbound/root.key"
Verify that /var/lib/unbound/root.key exists.
If not its location is distro dependent, it could be in /usr/share/dns/root.key or somewhere else however if both exist then use /var/lib/unbound/root.key

Apache config:
# Configuration file: /etc/unbound/unbound.conf
# Includes files: /etc/unbound/unbound.conf.d/unbound.conf
# https://nlnetlabs.nl/documentation/unbound/unbound.conf

# NOTE: Quick copy and apply config file
# sudo cp unbound.conf /etc/unbound/unbound.conf.d/unbound.conf
# sudo unbound-checkconf /etc/unbound/unbound.conf.d/unbound.conf
# sudo systemctl restart unbound

# Server Options
server:

    #
    # General options
    #

    # Interface to use to connect to the network
    # This interface is listened to for queries from clients, and answers to clients are given from it
    # If an interface name is used instead of an ip address, the list of ip addresses on that interface are used.
    # the default is to listen to localhost
    interface: 127.0.0.1
    #interface: ::0
    interface: 192.168.4.100

    # Allows you to bind to IP addresses that are nonlocal or do not exist,
    # like when the network interface or IP address is down (Default no)
    # NOTE: This is needed because on boot, the above specified interface might not yet be configured
    ip-freebind: yes

    # The port number, default 53, on which the server responds to queries
    # 853 is the default DoT port
    port: 53

    # By default only localhost is allowed, the rest is refused
    access-control: 127.0.0.1/8 allow
    # NOTE: Excluding access from gateway
    #access-control: 192.168.4.1/24 allow

    # Interface to use to connect to the network
    # This interface is used to send queries to authoritative servers and receive their replies
    # If none are given the default (all) is used
    # Outgoing queries are sent via a random outgoing interface to counter spoofing
    outgoing-interface: 192.168.4.100

    # Permit Unbound to open this port or range of ports for use to send queries
    # By default only ports above 1024 that have not been assigned by IANA are used
    # A larger number of permitted outgoing ports increases resilience against spoofing attempts
    # outgoing-port-permit:

    # If given, after binding the port the user privileges are dropped (Default is "unbound")
    username: "unbound"

    # The Unbound server forks into the background as a daemon
    # Set the value to no when Unbound runs as systemd service (Default is yes)
    do-daemonize: no

    # Default is nothing, using builtin hints for the IN class
    # The default may become outdated, when servers change, therefore it is good practice to use a root-hints file
    # NOTE: This file is part of "dns-root-data" package
    root-hints: "/usr/share/dns/root.hints"

    # The value of the Differentiated Services Codepoint (DSCP) in the
    # differentiated services field (DS) of the outgoing IP packet headers.
    # ip-dscp:

    #
    # Optimization
    # https://nlnetlabs.nl/documentation/unbound/howto-optimise/
    #

    # The number of threads to create to serve clients
    # Use 1 for  no threading
    num-threads: 4

    # Number of slabs in the message cache
    # Power of 2 close to num-threads
    msg-cache-slabs: 4

    # Number of bytes size of the message cache
    # Default is 4 megabytes.
    msg-cache-size: 20m

    # Number of slabs in the RRset cache
    # Power of 2 close to num-threads
    rrset-cache-slabs: 4

    # Number of bytes size of the RRset cache
    # Default is 4 megabytes
    # Use roughly twice as much rrset cache memory
    rrset-cache-size: 40m

    # Number of slabs in the key cache
    # Power of 2 close to num-threads
    key-cache-slabs: 4

    # Number of bytes size of the key cache
    # Default is 4 megabytes
    # NOTE: Recommended size unknown, using same as msg-cache-size
    key-cache-size: 20m

    # Number of slabs in the infrastructure cache
    # Power of 2 close to num-threads
    infra-cache-slabs: 4

    #
    # Privacy
    #

    # If enabled id.server and hostname.bind queries are refused
    hide-identity: yes

    # If enabled version.server and version.bind queries are refused
    hide-version: yes

    # If enabled the HTTP header User-Agent is not set
    hide-http-user-agent: no

    # Send minimum amount of information to upstream servers
    qname-minimisation: yes

    #
    # Hardening
    #

    # Very small EDNS buffer sizes from queries are ignored
    harden-short-bufsize: yes

    # Will trust glue only if it is within the servers authority.
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes bogus.
    # If turned off you run the risk of a downgrade attack that disables security for a zone.
    harden-dnssec-stripped: yes

    # The nxdomain must be secure
    harden-below-nxdomain: yes

    #
    # Logging
    #

    # Level 0 means no verbosity, only errors
    # Level 1 (default) gives operational information
    verbosity: 1

    # Send log messages to log facility LOG_DAEMON (rsyslog)
    use-syslog: yes

    # Prints one line per query to the log
    log-queries: no

    # Prints one line per reply to the log
    log-replies: no

    # Prints the word "query" and "reply" with log-queries and log-replies
    log-tag-queryreply: yes

    # Print log lines that say why queries return SERVFAIL to clients
    log-servfail: yes

    # Print log lines to inform about local zone actions.
    log-local-actions: no

    # Have the validator print validation failures to the log (Default is 0, off)
    val-log-level: 1

    #
    # Caching (in seconds)
    #

    # Time to live maximum for RRsets and messages in the cache
    cache-max-ttl: 21600 # 6h (Default is 86400 - 1day)

    # Time to live minimum for RRsets and messages in the cache
    cache-min-ttl: 0

    # Time to live maximum for negative responses
    cache-max-negative-ttl: 3600 # 1h

    # Time to live for entries in the host cache
    # The host cache  contains  roundtrip  timing, lameness and EDNS support information
    # Default is 900
    # NOTE: By setting the infra-ttl lower, unbound will probe servers that are not responsive more aggressively
    # https://unbound.docs.nlnetlabs.nl/en/latest/reference/history/info-timeout-server-selection.html
    infra-host-ttl: 60

    #
    # Connection
    #

    # Enable or disable whether IPv4 queries are answered or issued
    do-ip4: yes

    # Enable or disable whether IPv6 queries are answered or issued
    do-ip6: no

    # Enable or disable whether TCP queries are answered or issued
    do-tcp: yes

    # Enable or disable whether UDP queries are answered or issued
    do-udp: yes

    #
    # TLS
    #

    # The upstream queries use TLS only for transport
    tls-upstream: yes

    # The port number on which to provide TCP TLS service
    tls-port: 853

    # These certificates are used for authenticating connections made to outside peers
    # NOTE: To update run: sudo update-ca-certificates
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

    #
    # DNSSEC
    #

    # Uses the DNSSEC NSEC chain to synthesize NXDOMAIN and other denials, using information from previous NXDOMAINs answers.
    # It helps to reduce the query rate towards targets that get a very high nonexistent name lookup rate.
    aggressive-nsec: yes

    # This check sees if RRSIGs are present in the answer, when dnssec is expected,
    # and retries another authority if RRSIGs are unexpectedly missing.
    disable-dnssec-lame-check: no

    # "validator iterator" will turn on DNSSEC validation
    module-config: "validator iterator"

    # Mark bogus messages as indeterminate, otherwise SERVFAIL
    # For messages that are found to be secure the AD bit is set in replies
    val-permissive-mode: no

    # Count of validation restarts with another authority in case of failed validation
    val-max-restart: 5

    # TTL for data that has failed validation
    val-bogus-ttl: 60

    # The following line will configure unbound to perform cryptographic
    # DNSSEC validation using the root trust anchor.
    # Unbound needs rw access to this directory
    # NOTE: Automatically updated, or update with:
    # sudo unbound-anchor -v -a "/var/lib/unbound/root.key" -r "/usr/share/dns/root.hints" -c "/etc/ssl/certs/ca-certificates.crt"
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    #auto-trust-anchor-file: "/usr/share/dns/root.key"

# There may be multiple forward-zone: clauses.
# Each with a name: and zero or more hostnames or IP addresses.
# For the forward zone this list of nameservers is used to forward the queries to.
forward-zone:

    # Name of the forward zone.
    # This is the full domain name of the zone.
    name: "."

    # Enabled or disable whether the queries to this forwarder use TLS for transport
    forward-tls-upstream: yes

    # If enabled, data inside the forward is not cached
    # This is useful when you want immediate changes to be visible (Default is no)
    forward-no-cache: no

    # The servers listed as forward-host: and forward-addr: have to handle further recursion for the query
    # https://github.com/DigitaleGesellschaft/DNS-Resolver
    # https://www.quad9.net/support/faq/

    # IPv4 primary
    forward-addr: 185.95.218.42@853#dns.digitale-gesellschaft.ch
    forward-addr: 9.9.9.9@853#dns.quad9.net

    # IPv4 secondary
    forward-addr: 185.95.218.43@853#dns.digitale-gesellschaft.ch
    forward-addr: 149.112.112.112@853#dns.quad9.net

    # IPv6 primary
    forward-addr: 2a05:fc84::42@853#dns.digitale-gesellschaft.ch

    # Secure IPv6 primary: Blocklist, DNSSEC, No EDNS Client-Subnet
    forward-addr: 2620:fe::fe@853#dns.quad9.net

    # IPv6 secondary
    forward-addr: 2a05:fc84::43@853#dns.digitale-gesellschaft.ch

    # Secure IPv6 secondary: Blocklist, DNSSEC, No EDNS Client-Subnet
    forward-addr: 2620:fe::9@853#dns.quad9.net

# If this is enabled, the unbound-control(8) utility can be used to send commands to the running Unbound server
remote-control:

    # The option is used to enable remote control (default is no)
    control-enable: no

    # Give IPv4 or IPv6 addresses or local socket path to listen on for control commands.
    # If you set it to an absolute path, a unix domain socket is used.
    # This socket does not use the certificates and keys, so those files need not be present.
    # To restrict access, Unbound sets permissions on the file to the user and group that is configured,
    # the access bits are set to allow the group members to access the control socket file.
    # By default localhost (127.0.0.1 and ::1) is listened to.
    control-interface: /run/unbound.ctl

    # The port number to listen on for IPv4 or IPv6 control interfaces, (default is 8953)
    control-port: 8953

Run these commands below to make sure distro provided files don't mess up with our configuration.
But verify the 2 files exist in /etc/unbound/unbound.conf.d, their actual location may be distro dependent.
Bash:
sudo mv "/etc/unbound/unbound.conf.d/remote-control.conf" "/etc/unbound/unbound.conf.d/remote-control.conf.bak"
sudo mv "/etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf" "/etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf.bak"

When you're done with config customization, check for errors in main file which includes our unbound configuration file:
Bash:
sudo unbound-checkconf -f /etc/unbound/unbound.conf

Make sure there is only one *.conf file in /etc/unbound/unbound.conf.d (the one provided above), others should be renamed with *.bak extension.

You want to configure rsyslog if you enabled loging in config file.
Create a new file named in /etc/rsyslog.d and name it unbound.conf
Copy code below into it and save:
Bash:
# Configuration file: /etc/rsyslog.conf
# Includes files: /etc/rsyslog.d/
# https://www.rsyslog.com/doc/configuration/filters.html

# Log messages generated by unbound application
if $programname == "unbound" then /var/log/unbound/unbound.log

# stop processing it further
& stop

If you enabled loging you also want to rotate logs, here is config for rotatelog.
Create new file in /etc/logrotate.d and name it unbound, then copy/paste code below into it and save:
Bash:
# Configuration file: /etc/logrotate.conf
# Includes files: /etc/logrotate.d/
# https://linux.die.net/man/8/logrotate

# Rotate unbound logs

/var/log/unbound/unbound.log {
    daily
    rotate 10
    minsize 512k
    compress
    missingok
    notifempty
    noolddir
    dateext
    dateformat -%d.%m.%Y
    create
    maxage 30
    postrotate
        # When logrotate move the files, the services keep writing to the same file.
        # Sending the HUP signal to crond will force it to close existing file handle and open new file handle to the original path
        /usr/bin/systemctl kill -s HUP rsyslog.service > /dev/null 2>&1 || true
    endscript
}

Finally apply changes:

Bash:
# NOTE: logrotate isn't meant to be enabled
sudo systemctl enable unbound
sudo systemctl enable rsyslog

sudo systemctl restart unbound
sudo systemctl restart rsyslog
sudo systemctl restart logrotate

sudo systemctl status rsyslog
sudo systemctl status logrotate
sudo systemctl status unbound

Enable DNS resolving with unbound

Unbound will listen by default on localhost (127.0.0.1) on port 53,
therefore NetworkManager UI must be instructed to use this address for DNS.
If you use systemd-networkd or some other manager configure it to use 127.0.0.1 for DNS.

To apply restart NetworkManager or which ever manager you use and configured:
Bash:
sudo systemctl restart NetworkManager

The change in NetworkManager UI can be verified with:
Bash:
cat /etc/resolv.conf
It should list address 127.0.0.1

If it doesn't work and you lose internet access, you can simply undo changing DNS in NetworkManager.

Testing DNS

Now the moment of truth, we want to confirm that both DNS encryption and DNSSEC work, run this:
Bash:
dig +dnssec example.com

If you have no dig command, install it with your package manager (package name is distro dependent).

To confirm DNSSEC works there must be ad flag in header section and RRSIG in answer section, see sample output below:

dns.png


Loging

Extended loging can be enabled in /etc/unbound/unbound.conf.d/unbound.conf

  • /etc/rsyslog.d/unbound.conf configuration instructs rsyslog to write unbound logs to specific log file
  • /etc/logrotate.d/unbound configuration contains code to rotate log file

This concludes this tutorial, if something doesn't work as expected or you have questions let me know.
 
Last edited:


Using Waterfox Browser, my dns etc settings are as below:

1778631243469.png
 
@Condobloke
All web browsers have this option but I prefer system wide config because then have a choice which DNS server to use and can set plenty of other options, can cache queries and clean cache etc.

Web browsers force you to use 1 DNS they choose and some don't even tell you which DNS that is, there's also no DNSSEC in place.
System wide DNS resolver also handles all DNS request of all programs on computer not just those initiated by web browser.

You can also host it to your local network.
 
Solid guide muh dude!

You can also configure DNS Encryption at the router level, then configure DNS hijacking on port 53 and redirect to capture rogue queries from devices hard coded to phone home to their manufacturers choice DNS. I do this in conjunction with adguard and set my choice DNS through that. This covers the entire network to kill 100 birds with 2 stones so to speak.
 


Follow Linux.org

Members online


Top