Ubuntu Networking, Untangled: netplan, NetworkManager, networkd, and systemd-resolved — and Why Your DNS Comes from the Wrong Place

root@ubuntu:~# resolvectl status
root@ubuntu:~# resolvectl status # what DNS does this box actually believe right now

Ubuntu Networking, Untangled
netplan, NetworkManager, networkd, and systemd-resolved — and Why Your DNS Comes from the Wrong Place

Ubuntu networking is confusing in a specific way: there are three layers doing the same job, and nobody tells you which one is actually in charge on your box. You edit a file, the change works, you reboot, and it is gone. You set a DNS server, the box ignores it. You join the box to Active Directory, and DNS stops working for the domain you just joined. None of this is your fault. It is the stack, and the stack is explainable — once you know which layer owns what, and which file you are actually supposed to edit.

# the stack, in order — who owns what
netplan is the configuration. YAML files in /etc/netplan/ — usually 50-cloud-init.yaml on a cloud or template install, or a file you write yourself — describe what the network should look like: which interface, DHCP or static, which DNS servers, which routes. netplan itself configures nothing. It is a description, and it generates configuration for the layer below it.
The renderer is what applies it. A netplan file names a renderer: networkd or NetworkManager. On Ubuntu Server the default renderer is networkd (systemd-networkd); on Ubuntu Desktop it is NetworkManager. The renderer reads the generated configuration and owns the interfaces, the addresses, and the DHCP client. Two renderers, one box, and only one of them is in charge — check which with grep renderer /etc/netplan/*.yaml, because editing NetworkManager’s configuration on a networkd box does nothing, and the reverse is equally silent.
systemd-resolved is the DNS layer. It is a local caching resolver that listens on 127.0.0.53, and it is what your applications actually ask when they resolve a name. It gets its upstream servers from the renderer (from DHCP, or from the DNS you set in netplan), and it serves answers from its cache. The chain is: application → resolved (127.0.0.53) → upstream DNS → answer. Every DNS problem on Ubuntu lives somewhere in that chain, and the first step in debugging is always finding which link is broken, not changing a setting at random and hoping.
The rule that saves you: edit netplan, never the generated files, and never the files the layers below it own. netplan apply regenerates the renderer’s configuration and restarts it. If you edited a generated file by hand, the next netplan apply — or the next reboot, or the next cloud-init run — will overwrite it, and your change will vanish without a trace. That is not a bug. That is the design, and it is the design that eats everyone’s afternoon exactly once.
# resolv.conf is a symlink, and that is the whole story — the file everyone edits, and nobody should
On a modern Ubuntu box, /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf. Its contents are generated by systemd-resolved, and they say, in effect: “ask 127.0.0.53”. That is correct. The nameserver in that file is not your DNS server; it is the local stub, and the real upstream servers live in resolved’s configuration, per link. Editing resolv.conf directly does nothing durable — resolved regenerates it — and the edit that “worked” for ten minutes worked only until resolved rewrote the file.
Where DNS actually comes from, in order: the DNS servers you set in netplan (per interface, in the YAML), then the DNS servers DHCP hands out (if the interface uses DHCP and you did not override them), then the global fallback in /etc/systemd/resolved.conf. The per-link setting wins over DHCP; DHCP wins over the global fallback. If you want a fixed DNS server on a DHCP interface, set it in netplan under that interface — do not set it in resolved.conf and wonder why DHCP still wins, and do not edit resolv.conf and wonder why it reverts.
The commands that show the truth:
resolvectl status — every link, its DNS servers, its search domains, and which one is the default route for DNS.
resolvectl query example.com — ask resolved, and see which server answered.
ls -l /etc/resolv.conf — if it is not a symlink to the stub, something else is managing DNS on this box, and that something is the first thing to find.
The two modes of resolv.conf, and why they matter: the stub (/run/systemd/resolve/stub-resolv.conf, nameserver 127.0.0.53) gives you resolved’s caching, per-link routing, and split DNS. The alternative, /run/systemd/resolve/resolv.conf, lists the upstream servers directly and bypasses the stub — useful when an application does not work through the stub, but you lose the per-link behaviour with it. If a legacy application cannot resolve through 127.0.0.53, pointing the symlink at the upstream file is a legitimate fix, and it is a fix you make deliberately, once, not an edit you keep re-applying.
If you have ever edited resolv.conf, rebooted, and found your edit gone, you did not do anything wrong. You edited a generated file. The generated file is not the configuration. netplan is the configuration, resolved is the resolver, and resolv.conf is the output.
# DHCP: the client, the lease, and the DNS it brings — what DHCP actually hands you
The DHCP client depends on the renderer. Under networkd, the DHCP client is networkd itself. Under NetworkManager, it is NetworkManager’s internal client (or dhclient, on older setups). Either way, the lease brings more than an address: it can bring a default gateway, DNS servers, a search domain, an NTP server, and a hostname. The DNS servers and the search domain are the ones that cause the trouble in this post, because they arrive silently and override what you thought you had set.
The two DHCP settings that matter in netplan:
dhcp4: true — take the address, gateway, DNS, and search domain from DHCP.
dhcp4-overrides:
  use-dns: false
— take the address and gateway from DHCP, but not the DNS servers. This is the setting for a box whose DNS you manage yourself, on a network whose DHCP hands out DNS you do not want. Set it, set your DNS explicitly in the same netplan file, and the DHCP server stops overwriting your resolvers.
The lease, when it goes stale: a box that keeps an old lease after a network change — a clone, a moved VM, a DHCP server that changed its scope — will keep the old DNS servers until the lease renews. Force it with a renderer restart (systemctl restart systemd-networkd, or nmcli connection down/up under NetworkManager), and check resolvectl status afterwards to confirm the new servers arrived. A lease is a cache, and caches go stale.
The hostname from DHCP, and why it matters: a DHCP server can hand a box a hostname, and a box that accepts it will rename itself on every lease renewal. If your hostname keeps changing, that is why — and the fix is the same overrides block, with use-hostname: false. A server whose name changes by itself is a server whose monitoring, logging, and Kerberos identity all break at once.
# NetworkManager vs networkd — which one, and when to switch
networkd (systemd-networkd) is the server default. It is declarative, file-driven, and predictable: the configuration is text, the state is visible with networkctl status, and there is no GUI, no applet, and no interactive state to drift. It is the right renderer for a server, a VM, and anything you manage with configuration management. Its weakness is interactivity: it does not handle Wi-Fi roaming, VPNs that come and go, or a laptop moving between networks, because that was never its job.
NetworkManager is the desktop and laptop default. It handles Wi-Fi, VPNs, mobile broadband, and connections that appear and disappear, and it remembers them per-user and per-connection. It is the right renderer for a machine a human carries around. Its weakness on a server is exactly its strength on a laptop: it has interactive state, and a server whose network depends on interactive state is a server that can surprise you after a reboot.
Switching renderers, safely: change the renderer in the netplan YAML, run netplan apply, and verify with networkctl status or nmcli general status — whichever you switched to should now own the interfaces, and the other should show nothing. Do it from the console or from a second session, the same way you would a firewall change: a renderer switch that goes wrong takes the network with it, and you do not want to be locked out of the box you just reconfigured. And never run both at once on the same interface — two renderers fighting over one interface is a configuration that works until it does not, and the failure looks like a network problem, not a configuration problem.
# the AD pain point — DNS that won’t come from the domain controller
The setup: an Ubuntu box joined to Active Directory — usually with realmd and sssd — on a network where the domain controllers are the DNS servers for the AD domain, and DHCP hands out something else (a router, a public resolver, the ISP’s DNS). The box joins the domain fine, and then it cannot resolve the domain’s own records: the DC, the Kerberos SRV records, the hosts in the AD DNS zone. Authentication fails, and the failure looks like a Kerberos problem or an sssd problem, when it is a DNS problem wearing a Kerberos costume.
Why it happens: resolved sends a query to the DNS servers it knows about, and the servers it knows about are the DHCP ones, which know nothing about the AD zone. The AD DNS zone is a private zone, served only by the DCs, and a public or ISP resolver will answer “that domain does not exist” — confidently, and wrongly. The fix is not to make the DCs the only DNS servers for everything; it is to tell resolved that the AD domain belongs to the DCs, and everything else belongs to the existing servers. That is split DNS, and resolved does it natively, per link.
Fix one — pin DNS per link in netplan (the durable fix):
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: true
      dhcp4-overrides:
        use-dns: false
      nameservers:
        addresses: [10.0.0.10, 10.0.0.11]
        search: [ad.example.com, example.com]

Then netplan apply and resolvectl status to confirm the DCs are the link’s DNS servers and the AD domain is in the search list. The DHCP server still hands out the address and gateway; it no longer hands out DNS. The DCs answer for the AD zone, and they forward everything else upstream, which is what a DC’s DNS is configured to do.
Fix two — routing domains in resolved (the surgical fix): when the box needs the DCs for the AD domain and the existing DNS for everything else, set a routing domain on the link: resolvectl domain eth0 ad.example.com (or the equivalent in netplan, nameservers: search: with the domain, plus the DC as a per-link DNS server). Queries for ad.example.com go to the DCs; everything else goes to the existing servers. Verify with resolvectl query dc01.ad.example.com and resolvectl query example.com — the first should answer from a DC, the second from the normal path, and resolvectl status should show the routing domain on the link.
The sssd side, briefly: sssd needs to resolve the DC and its SRV records (_kerberos._tcp, _ldap._tcp) to find the domain, and it needs the clock right — Kerberos fails on a clock skew of minutes, and a box whose DNS is wrong will fail Kerberos in a way that looks like a clock problem. Check DNS first, with resolvectl, before touching sssd or Kerberos. And check chronyc tracking second. The number of AD join failures that are actually DNS or time, not AD, is embarrassingly high, and I have paid that tax myself.
The search domain, and why short names fail: if ping dc01 fails but ping dc01.ad.example.com works, the search domain is missing, not the DNS server. The search list comes from DHCP or from netplan’s nameservers: search: — and a DHCP server that hands out no search domain leaves the box unable to complete short names. Set it explicitly in netplan, and the short names start working.
AD DNS is not a special case. It is split DNS, and split DNS is a normal thing that resolved does well — once you tell it which domain belongs to which server, instead of expecting it to guess from a DHCP lease that never mentioned the domain at all.
# debugging: the order that works — stop changing settings at random
1. Which renderer? grep renderer /etc/netplan/*.yaml — and networkctl status or nmcli general status to confirm it owns the interfaces.
2. What does the link have? ip addr and ip route — address, gateway, and whether the interface is up at all. No address means the problem is DHCP or the link, not DNS.
3. What does resolved believe? resolvectl status — DNS servers, search domains, routing domains, per link. If the servers listed are not the ones you set, find which layer set them: netplan, DHCP, or the global fallback.
4. Ask resolved directly. resolvectl query host.example.com — if it fails, ask the upstream directly with dig @10.0.0.10 host.example.com to separate a resolved problem from an upstream problem. If dig works and resolvectl does not, the problem is in resolved’s configuration. If dig fails too, the problem is the upstream server or the network to it.
5. Check the symlink. ls -l /etc/resolv.conf — if it is not the stub, something else owns DNS, and that something is the first thing to identify before changing anything else.
# the cheat sheet — keep this, not the man page
edit the config
netplan YAML in /etc/netplan/, then netplan apply — never the generated files, never resolv.conf
which renderer
grep renderer /etc/netplan/*.yaml; server = networkd, desktop/laptop = NetworkManager
what DNS does the box believe
resolvectl status
DHCP DNS you do not want
dhcp4-overrides: use-dns: false, then set nameservers in netplan
AD DNS
pin the DCs per link in netplan, with the AD domain in the search list — or a routing domain in resolved
short names fail, long names work
search domain is missing — set it in netplan
separate resolved from upstream
dig @<server> vs resolvectl query

Commands verified against Ubuntu 22.04/24.04 behaviour (netplan, systemd-networkd, NetworkManager, systemd-resolved) and Active Directory join practice with realmd/sssd, November 7, 2026. Renderer defaults and file locations vary by Ubuntu release and install type — check the renderer on the box in front of you before editing. Example addresses and domains (10.0.0.10, ad.example.com) are illustrative — substitute your own.

Scroll to Top