Encrypted DNS
By default, DNS traffic is sent in plain text over the internet; it can be monitored or spoofed by any party along the path, including your ISP. DNSSEC authenticates the DNS records themselves, but can't stop your ISP monitoring domains or dropping queries.
Encrypted DNS protocols aim to address this hole by encrypting queries and responses in transit between DNS resolvers and clients; the most widely deployed ones are DNS over HTTPS (DoH), DNS over TLS (DoT), and DNSCrypt.
NixOS has modules for multiple encrypted DNS proxies, including dnscrypt-proxy 2 and Stubby. services.dnscrypt-proxy2
is generally recommended, as it has the widest protocol and feature support, and is written in a memory-safe language.
Setting nameservers
No matter what proxy you use, you should set your DNS nameservers statically and make sure that your network manager won't override your carefully set nameservers with some random settings it received over DHCP.
{
networking = {
nameservers = [ "127.0.0.1" "::1" ];
# If using dhcpcd:
dhcpcd.extraConfig = "nohook resolv.conf";
# If using NetworkManager:
networkmanager.dns = "none";
}
# Make sure you don't have services.resolved.enable on.
}
If you'd prefer to keep using resolvconf then you can set networking.resolvconf.useLocalResolver
instead. Note that it uses the IPv4 loopback address only.
dnscrypt-proxy2
Example configuration
{
services.dnscrypt-proxy2 = {
enable = true;
settings = {
ipv6_servers = true;
require_dnssec = true;
sources.public-resolvers = {
urls = [
"https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md"
"https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md"
];
cache_file = "/var/lib/dnscrypt-proxy2/public-resolvers.md";
minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
};
# You can choose a specific set of servers from https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/public-resolvers.md
# server_names = [ ... ];
};
};
systemd.services.dnscrypt-proxy2.serviceConfig = {
StateDirectory = "dnscrypt-proxy";
};
}
See the upstream example configuration file for more configuration options.
Local network - Forwarding rules
Maybe you'd like queries for your local domain to go to your router, and not to an upstream DNS resolver. By doing so, names of your local online devices can be found. For this you have to create a file with forwarding rules which you then include in your config:
{
services.dnscrypt-proxy2 = {
enable = true;
settings = {
...
forwarding_rules = "/etc/nixos/services/networking/forwarding-rules.txt";
...
};
};
....
}
Using alongside another DNS server
DNS authoritative nameservers are tied to port 53, and the Linux /etc/resolv.conf
doesn't allow specifying a different port for resolvers either. This leads to conflicts if you have another DNS server you need to expose externally on port 53 (e.g. an authoritative DNS server for your domains, or acme-dns), and can't easily run it on a separate IP to dnscrypt-proxy2 (e.g. your authoritative DNS server listens on ::
/0.0.0.0
). You can resolve this by running the proxy on a different port and forwarding loopback traffic on port 53 to it:
{
networking.nameservers = [ "::1" ];
services.dnscrypt-proxy2 = {
enable = true;
settings = {
listen_addresses = [ "[::1]:51" ];
# ...
};
};
# Forward loopback traffic on port 53 to dnscrypt-proxy2.
networking.firewall.extraCommands = ''
ip6tables --table nat --flush OUTPUT
${lib.flip (lib.concatMapStringsSep "\n") [ "udp" "tcp" ] (proto: ''
ip6tables --table nat --append OUTPUT \
--protocol ${proto} --destination ::1 --destination-port 53 \
--jump REDIRECT --to-ports 51
'')}
'';
}
Note that you can still access the other DNS server locally through the non-loopback interface (e.g. by using your server's external IP).
Stubby
Stubby is a very lightweight resolver (40kb binary) that performs DNS-over-TLS, and nothing else. While stubby can be used as a system resolver on its own, it is typically combined with another resolver (such as unbound) to add caching and forwarding rules for local domains. See the options documentation for services.stubby.*
for configuration.
Example configuration for Cloudflare. Note that digests change and need to be updated:
{
services.stubby = {
enable = true;
settings = pkgs.stubby.passthru.settingsExample // {
upstream_recursive_servers = [{
address_data = "1.1.1.1";
tls_auth_name = "cloudflare-dns.com";
tls_pubkey_pinset = [{
digest = "sha256";
value = "GP8Knf7qBae+aIfythytMbYnL+yowaWVeD6MoLHkVRg=";
}];
} {
address_data = "1.0.0.1";
tls_auth_name = "cloudflare-dns.com";
tls_pubkey_pinset = [{
digest = "sha256";
value = "GP8Knf7qBae+aIfythytMbYnL+yowaWVeD6MoLHkVRg=";
}];
}];
};
};
}
To update digests get the TLS certificate that signs the responses and calculate the digest:
echo | openssl s_client -connect '1.1.1.1:853' 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
Or using kdig
from knot-dns
kdig -d @1.1.1.1 +tls-ca +tls-host=one.one.one.one example.com