Service DNS and Cross-Namespace Access
Objective
Every Service gets a DNS name, served by CoreDNS inside the cluster: <service>.<namespace>.svc.cluster.local. Pods use it to reach each other without knowing any IP. Short names work only inside the same namespace, which is the source of the classic "it resolves from here but not from there" bug when an application in team-a talks to a database in shared. This concept covers the name formats, how the Pod's resolv.conf search list makes short names work, why ndots:5 matters for external lookups, and how to debug resolution.
Use Cases
- Pointing an application at a database or cache in a
sharednamespace. - Using the same manifests in several namespaces without hardcoding IPs.
- Understanding slow external DNS lookups from Pods.
- Debugging "unknown host" or "could not resolve host" errors in a container.
Deep Dive
The names a Service gets
For a Service db in namespace shared:
| Name | Resolves from |
|---|---|
db |
Pods in shared only |
db.shared |
any namespace |
db.shared.svc |
any namespace |
db.shared.svc.cluster.local |
any namespace (fully qualified) |
A ClusterIP Service resolves to its virtual IP. A headless Service (clusterIP: None) resolves to the IPs of its ready Pods directly, and StatefulSet Pods behind one also get individual names (db-0.db.shared.svc.cluster.local). Named ports get SRV records (_http._tcp.api.team-a.svc.cluster.local).
Pods themselves do not get useful DNS names by default; always address Services.
Why short names only work in the same namespace
Every Pod gets an /etc/resolv.conf generated by the kubelet. Verified from a Pod in namespace app on k3s:
plaintextsearch app.svc.cluster.local svc.cluster.local cluster.local nameserver 10.43.0.10 options ndots:5
(On MicroK8s the nameserver is 10.152.183.10.)
The resolver tries each search suffix in order. db becomes db.app.svc.cluster.local first: found only if the Service lives in app. db.shared becomes db.shared.app.svc.cluster.local (miss), then db.shared.svc.cluster.local (hit). Verified: from namespace app, nslookup db failed with NXDOMAIN while wget http://db.infra and http://db.infra.svc.cluster.local both reached the Service in infra.
Rule of thumb for configuration: same namespace, short name; other namespace, at least service.namespace. Many teams always use the fully qualified form in config for readability; with a trailing dot (db.shared.svc.cluster.local.) it also skips the search list entirely.
ndots:5 and external names
ndots:5 means "a name with fewer than 5 dots is tried with every search suffix first". api.github.com has 2 dots, so the resolver asks for api.github.com.app.svc.cluster.local, api.github.com.svc.cluster.local, api.github.com.cluster.local (all NXDOMAIN, usually for both A and AAAA) before the real name. It works, it just costs extra queries on every uncached lookup. For chatty clients you can lower it per Pod:
plaintextspec: dnsConfig: options: - {name: ndots, value: "2"}
Many language runtimes and client libraries also cache DNS results for a while, which hides most of that cost, and also means a changed record is picked up late.
Debugging resolution
plaintext# a throwaway Pod in the namespace of the failing app kubectl -n team-a run dnstest --rm -it --image=busybox:1.37 --restart=Never -- sh / # cat /etc/resolv.conf / # nslookup db.shared / # nslookup kubernetes.default / # wget -qO- -T 3 http://api.team-a:80/healthz
Reading the results:
nslookup kubernetes.defaultfails: CoreDNS itself is down or unreachable (checkkubectl -n kube-system get pods -l k8s-app=kube-dns, and whether an egress NetworkPolicy blocks UDP/TCP 53).- The name resolves but the connection times out: DNS is fine; look at the Service's endpoints (
kubectl get endpointslices -l kubernetes.io/service-name=db -n shared) and NetworkPolicies. NXDOMAINfor a name you expect: wrong namespace, a typo, or the Service does not exist.
Verified gotcha: with a default-deny egress NetworkPolicy in the client's namespace, DNS itself fails (nslookup: write to '10.43.0.10': Connection refused), so every hostname looks "unknown" even though the real problem is network policy. Egress policies must allow port 53 to CoreDNS.
Cross-namespace access is a network question, not a DNS one
DNS makes every Service in the cluster resolvable from every namespace. Whether the connection is allowed depends only on NetworkPolicies. Resource references are different: a Pod can only mount ConfigMaps, Secrets and PVCs from its own namespace, so credentials for a shared database must be copied (or synced) into each consuming namespace.
ExternalName for aliases
An ExternalName Service is a DNS CNAME, useful to give an external or cross-namespace dependency a local name:
plaintextapiVersion: v1 kind: Service metadata: {name: db, namespace: team-a} spec: type: ExternalName externalName: db.shared.svc.cluster.local
Now db works as a short name inside team-a. There is no proxying and no port mapping, it is only DNS, and TLS clients will still see the target's hostname in the certificate check.
Trade-offs
- Short names vs fully qualified names in config. Short names keep manifests portable between namespaces for same-namespace dependencies; fully qualified names are explicit and immune to search-list surprises.
- Lower
ndotsvs default. Fewer wasted queries for external names, at the risk of breaking short-name lookups of multi-part names likedb.shared(1 dot is still under 2, fine; but review before lowering further). - ExternalName alias vs direct name. An alias hides where a dependency lives and makes moving it easy; it also hides where a dependency lives when you debug.