โ† Kubernetes Concepts

Service DNS and Cross-Namespace Access

Published on 2026-09-26ยทv1.0

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 shared namespace.
  • 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:

plaintext
search 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:

plaintext
spec: 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.default fails: CoreDNS itself is down or unreachable (check kubectl -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.
  • NXDOMAIN for 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:

plaintext
apiVersion: 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 ndots vs default. Fewer wasted queries for external names, at the risk of breaking short-name lookups of multi-part names like db.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.

Documentation Links