Set Up systemd-resolved Cleanly
Enable systemd-resolved's stub listener, configure DNS-over-TLS, set up conditional forwarding for split-horizon DNS, and debug with resolvectl.
Before you start
- ▸systemd version 239 or later (check with systemd --version)
- ▸Root or sudo access on the target system
- ▸NetworkManager or systemd-networkd managing your network interfaces
- ▸Basic familiarity with editing files in /etc/systemd/
systemd-resolved is the DNS resolver that ships with systemd and handles name resolution for the whole system through a single, manageable service. Many distributions ship it partially enabled—or quietly bypass it—leading to subtle breakage. This guide shows you how to take full control: enable the stub listener, configure DNS-over-TLS, set per-link conditional forwarding, and debug problems with resolvectl.
How systemd-resolved Works
systemd-resolved exposes three interfaces:
- Native D-Bus API — used by NetworkManager, systemd-networkd, and other daemons to push per-link DNS settings.
- NSS plugin (
nss-resolve) — makesgetaddrinfo()calls resolve through resolved directly. - Stub listener — a DNS server on
127.0.0.53:53that any application can target, including those that read/etc/resolv.conf.
The cleanest setup uses the stub listener: /etc/resolv.conf points to 127.0.0.53, and resolved handles everything from there. Distributions often ship a different symlink or a static file—fix that first.
Step 1: Enable and Start systemd-resolved
The service should be running before you touch any configuration.
sudo systemctl enable --now systemd-resolved
Confirm it is active:
systemctl status systemd-resolved
Step 2: Fix /etc/resolv.conf
systemd-resolved manages a generated resolv.conf at /run/systemd/resolve/stub-resolv.conf. Point the system file there with a symlink:
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
Verify the result—it should contain nameserver 127.0.0.53 and options edns0 trust-ad:
cat /etc/resolv.conf
On some systems (especially cloud images), /etc/resolv.conf is a plain file or a different symlink. The ln -sf above overwrites whatever was there. If NetworkManager is managing your connection, it will continue to push upstream DNS servers to resolved via D-Bus—that part keeps working regardless of which symlink you choose.
Step 3: Configure DNS-over-TLS
Edit /etc/systemd/resolved.conf. The file ships with almost everything commented out; uncomment and set the relevant keys:
sudo nano /etc/systemd/resolved.conf
A minimal DNS-over-TLS configuration using Cloudflare and Quad9 as fallback upstreams:
[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com
FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net
DNSOverTLS=opportunistic
DNSSEC=allow-downgrade
ResolveUnicastSingleLabel=no
DNSOverTLS values explained:
no— plain DNS only (default).opportunistic— tries TLS, silently falls back to plain DNS if TLS fails. Good starting point.yes— requires TLS; queries fail if TLS cannot be established. Use this only if you control or fully trust the upstream.
DNSSEC values explained:
allow-downgrade— validates signed zones, ignores unsigned zones. The practical choice for most networks.true— strict validation; unsigned zones that should be signed will fail. Can break corporate networks.
After editing, restart the service:
sudo systemctl restart systemd-resolved
Step 4: Conditional Forwarding Per Link or Domain
Conditional forwarding sends queries for specific domains to a specific DNS server—essential in split-horizon VPN setups or corporate environments where internal names resolve only on the internal DNS.
Using systemd-networkd drop-ins
If you manage interfaces with systemd-networkd, create or edit a .network file for the relevant interface. The example below forwards corp.example.com queries to an internal resolver:
sudo nano /etc/systemd/network/10-internal.network
[Match]
Name=eth0
[Network]
DHCP=yes
[DHCPv4]
UseDNS=no
[Network]
DNS=10.0.0.53
Domains=~corp.example.com ~example.internal
The tilde prefix (~domain) marks a domain as routing-only: resolved sends matching queries to that link's DNS server but does not use it for search-path expansion. Without the tilde, the domain is also appended as a search domain, which is usually not what you want for internal zones.
Using NetworkManager (most desktops)
NetworkManager passes DNS and domain settings to resolved automatically when you configure a connection. You can set routing domains in the GUI or via nmcli:
sudo nmcli connection modify "VPN-Office" ipv4.dns "10.0.0.53" ipv4.dns-search "~corp.example.com"
Apply without disconnecting:
sudo nmcli connection up "VPN-Office"
Manual per-link configuration with resolvectl
For temporary changes or scripting, push settings directly to a link:
sudo resolvectl dns eth0 10.0.0.53
sudo resolvectl domain eth0 "~corp.example.com" "~example.internal"
These changes last only until the link goes down or the service restarts. They are useful for debugging before you commit to a config file.
Step 5: Verify the Configuration
Check the global state, showing active upstreams, DNSSEC, and DNSOverTLS status:
resolvectl status
Output will show a global section and a per-link section. Look for DNS over TLS setting: opportunistic and that your upstream IPs appear under the correct link.
Test name resolution and confirm which server answered:
resolvectl query example.com
This prints the resolved addresses plus the server that answered and whether DNSSEC validated. It is more informative than dig or nslookup for troubleshooting resolved specifically.
Verify DNS-over-TLS is actually being used by checking statistics:
resolvectl statistics
The Current Transactions and cache hit lines will increment as you make queries. If TLS negotiation fails, resolved falls back and you will not see TLS errors here—use the debug steps below instead.
Troubleshooting
Queries failing after enabling DNSSEC
Some zones are deliberately unsigned or have broken signatures. Switch to allow-downgrade to isolate the problem:
resolvectl query --no-pager --type=DS example.com
If you see SERVFAIL with DNSSEC=true but the query works with resolvectl query --set-dnssec=no example.com, the zone has a DNSSEC issue upstream, not a local one.
Applications still using the wrong DNS server
Check that /etc/resolv.conf is actually the stub symlink and that /etc/nsswitch.conf has resolve before dns in the hosts line:
grep hosts /etc/nsswitch.conf
Expected output contains something like: hosts: mymachines resolve [!UNAVAIL=return] files mDNS4_minimal [NOTFOUND=return] dns. On systems where resolve is missing, install or reinstall the libnss-resolve package (Debian/Ubuntu) or ensure the systemd package is complete (Fedora/Arch).
# Debian/Ubuntu only
sudo apt install libnss-resolve
Enabling debug logging
Set the log level to debug temporarily without editing any file:
sudo resolvectl log-level debug
Then watch the journal:
journalctl -u systemd-resolved -f
Reset log level when done:
sudo resolvectl log-level info
Flush the cache
If you are testing config changes and seeing stale answers:
sudo resolvectl flush-cachesFrequently asked questions
- What is the difference between /run/systemd/resolve/resolv.conf and stub-resolv.conf?
- stub-resolv.conf points to 127.0.0.53 (the stub listener) and is the recommended symlink target. resolv.conf lists the actual upstream servers resolved is using—handy for debugging but bypasses the stub, meaning resolved cannot apply its caching or DNSSEC validation to those queries.
- Will NetworkManager overwrite my /etc/resolv.conf symlink?
- NetworkManager respects the stub symlink and pushes DNS settings to resolved via D-Bus rather than rewriting resolv.conf directly, as long as the symlink target is one of the two systemd-resolved paths it recognises. If NetworkManager is configured with dns=none in /etc/NetworkManager/NetworkManager.conf it will never touch resolv.conf at all.
- DNSOverTLS=yes breaks some of my queries. What should I check?
- With strict TLS mode, any upstream that cannot negotiate TLS on port 853 will cause failures. Verify the upstream supports DNS-over-TLS and that the hostname you provided in resolved.conf matches the server's certificate. Use opportunistic mode while testing, then switch to yes once confirmed.
- How do I make conditional forwarding work for a WireGuard or OpenVPN interface?
- Push the DNS server and routing domain to resolved as soon as the tunnel interface comes up—either through a systemd-networkd .network file, a NetworkManager connection profile, or a post-up script that calls resolvectl dns wg0 10.x.x.x and resolvectl domain wg0 ~internal.example.com.
- Does systemd-resolved support mDNS for local network discovery?
- Yes. Set MulticastDNS=yes in /etc/systemd/resolved.conf for global default, or per-link in a .network file. This allows .local name resolution. Note that resolved's mDNS implementation does not support service discovery (DNS-SD); for that, avahi is still the standard choice.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.