LinuxXXIII · DNSsystemd-resolved
systemd-resolved - the modern stub resolver
What you'll learn
- Describe what systemd-resolved does and when to use it
- Configure per-interface DNS via systemd-resolved
- Build split-horizon DNS with route-only (~) domains
- Explain why FallbackDNS is not a failover, and how it leaks internal names
- Use resolvectl for diagnosis and cache management
- Enable DNSSEC and DNS-over-TLS
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11
systemd-resolved is the local stub resolver that ships with modern systemd-based Linux. It runs on 127.0.0.53:53 and provides per-interface DNS configuration, a cache, optional DNSSEC validation, and DNS-over-TLS support. Most modern Ubuntu and Fedora hosts use it by default.
What it does
- Listens on 127.0.0.53:53 as the full stub resolver, and on
127.0.0.54:53 as the proxy stub (which forwards without
local caching or DNSSEC). It does not listen on
::1. - Receives DNS queries from applications.
- Forwards queries to upstream resolvers (per interface).
- Caches answers for the TTL of the record.
- Optionally validates DNSSEC chains.
- Optionally encrypts upstream queries with DNS-over-TLS.
The applications see a local resolver. The actual DNS traffic goes from systemd-resolved to the upstream.
Inspect the configuration
resolvectl status
Output:
Global
Protocols: -LLMNR -mDNS DNSOverTLS=opportunistic
DNSSEC: yes
Current DNS Server: 10.0.0.1
DNS Servers: 10.0.0.1 10.0.0.2
DNS Domain: example.com
Link 2 (eth0)
Current DNS Server: 10.0.0.1
DNS Servers: 10.0.0.1 10.0.0.2
DNS Domain: example.com
The Global section shows the default configuration. The
Link sections show per-interface configuration. systemd-
resolved tracks per-interface so that DHCP-provided DNS
servers are used when the interface is up.
Configure systemd-resolved
Edit /etc/systemd/resolved.conf:
[Resolve]
DNS=10.0.0.1 10.0.0.2
FallbackDNS=
Domains=example.com ~corp.example.com
DNSSEC=yes
DNSOverTLS=opportunistic
Cache=yes
DNSStubListener=yes
| Directive | Meaning |
|---|---|
DNS= | Global upstream resolvers. Used for every query not routed to a per-link server |
FallbackDNS= | Used only when no DNS server is known from any source. Not a failover for an unreachable DNS=. Left empty above, deliberately |
Domains= | Entries without ~ are search suffixes for single-label names. An entry prefixed with ~ is a route-only domain: queries for that zone go to this configuration’s servers |
DNSSEC=yes | Validate DNSSEC chains |
DNSOverTLS=opportunistic | Encrypt upstream queries if supported |
Cache=yes | Enable caching (default) |
DNSStubListener=yes | Listen on 127.0.0.53 (default) |
Apply:
sudo systemctl restart systemd-resolved
resolvectl status
FallbackDNS is not a failover
Two directives are routinely misread, and the pair of misreadings produces a configuration that leaks internal names to a third party.
FallbackDNS= is used only when systemd-resolved knows of no
DNS server at all - nothing from DNS=, nothing from a link,
nothing from /etc/resolv.conf. It does not step in when the
servers in DNS= stop answering; resolved retries and rotates
within DNS= for that. And if you never set FallbackDNS=,
systemd ships a compiled-in list of public resolvers and uses
that instead.
Split-horizon DNS with route-only domains
Routing a zone to a particular resolver is what the ~ prefix
does. Without it, Domains=corp.example.com only adds a search
suffix, so db-prod is tried as db-prod.corp.example.com -
useful, but it routes nothing.
The correct shape is a global default plus a per-link route:
# /etc/systemd/resolved.conf - the public default
[Resolve]
DNS=9.9.9.9 149.112.112.112
FallbackDNS=
Domains=~.
# /etc/systemd/network/10-corp.network - the internal zone
[Match]
Name=eth1
[Network]
DNS=10.0.0.1 10.0.0.2
Domains=corp.example.com ~corp.example.com ~10.in-addr.arpa
~. is the special route-only domain meaning “everything
else”: it makes the global servers the default route for any
query not claimed by a more specific route. ~corp.example.com
on the link claims that zone, and the longest matching route
wins, so internal names go to the internal resolvers and
nothing else does. The unprefixed corp.example.com is listed
as well because it is still wanted as a search suffix.
Verify the routing rather than assuming it:
resolvectl domain # which domains are routed to which link
resolvectl query db-prod.corp.example.com
# the output names the server that answered
Per-interface DNS
systemd-resolved picks up DNS configuration from the network management layer (NetworkManager, systemd-networkd). Configure per interface:
In NetworkManager:
sudo nmcli connection modify "Wired connection 1" \
ipv4.dns "10.0.0.1 10.0.0.2" \
ipv4.dns-search "example.com" \
ipv4.ignore-auto-dns yes
In systemd-networkd .network:
[Match]
Name=eth0
[Network]
Address=10.0.0.10/24
Gateway=10.0.0.1
DNS=10.0.0.1 10.0.0.2
Domains=example.com ~example.com
Reload the network manager and systemd-resolved picks up the new configuration.
Both spellings of the domain are present on purpose:
example.com makes it a search suffix, ~example.com routes
queries for that zone to this link’s servers. Listing only the
unprefixed form is the common mistake - it looks like split
DNS and routes nothing.
Query through systemd-resolved
resolvectl query example.com
resolvectl query -t AAAA example.com
resolvectl query -t MX example.com
Output shows the answer, the protocol used, and whether the answer was DNSSEC-validated.
Cache management
resolvectl statistics
Output:
DNSSEC supported by current servers: yes
Transactions
Current Transactions: 0
Total Transactions: 1234
Cache
Current Cache Size: 42
Hits: 1100
Misses: 134
A high hit rate is good - it means systemd-resolved is caching effectively and not re-querying upstream.
resolvectl flush-caches
Empties the cache. Useful during debugging or after suspected stale answers.
DNSSEC
systemd-resolved validates DNSSEC chains by default if
DNSSEC=yes:
resolvectl query example.com
Output includes Verification: OK (or failure). DNSSEC
validation failures surface as SERVFAIL rather than NXDOMAIN.
DNS-over-TLS (DNSOverTLS=opportunistic):
- Upstream resolver must support DNS-over-TLS on port 853.
- If not, falls back to plain DNS (the “opportunistic” part).
- If
yes, fails if upstream does not support TLS.
For internal networks, opportunistic is the right choice.
For production, yes if you trust the upstream.
Common diagnostic recipes
# What does the local stub say?
resolvectl query example.com
# What does the configured upstream say?
dig @10.0.0.1 example.com
# Force a fresh query (bypass cache)
resolvectl query example.com
# Cache hit if you see "cache hit" in the output
# Check cache statistics
resolvectl statistics
# Flush the cache after a DNS change
resolvectl flush-caches
# Check DNSSEC validation
resolvectl query -t A dnssec-failed.org
# Should return SERVFAIL with "DNSSEC validation failed"
Knowledge check
Knowledge check · 5 questions
Q1. What is the local stub address that systemd-resolved listens on?
Q2. systemd-resolved always validates DNSSEC if enabled.
Q3. Which of the following are valid /etc/systemd/resolved.conf directives? Select all that apply.
Q4. When does systemd-resolved use the servers listed in FallbackDNS=?
Q5. An engineer configures DNS=10.0.0.1, FallbackDNS=8.8.8.8, Domains=corp.example.com and expects internal names to go to 10.0.0.1 and public names to Google. What actually happens?
Passing score: 75%. Answers are checked in this browser.