Skip to main content
RunBook Academy

LinuxXXIII · DNSsystemd-resolved

systemd-resolved - the modern stub resolver

Intermediate⏱ ~15 minresolvectlsystemctl

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

Not yet marked complete on this device.

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
DirectiveMeaning
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=yesValidate DNSSEC chains
DNSOverTLS=opportunisticEncrypt upstream queries if supported
Cache=yesEnable caching (default)
DNSStubListener=yesListen 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

  1. Q1. What is the local stub address that systemd-resolved listens on?

  2. Q2. systemd-resolved always validates DNSSEC if enabled.

  3. Q3. Which of the following are valid /etc/systemd/resolved.conf directives? Select all that apply.

  4. Q4. When does systemd-resolved use the servers listed in FallbackDNS=?

  5. 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.