ExternalName Service
An ExternalName Service is the one Service type that has no virtual IP, no endpoints, no kube-proxy involvement, and no traffic interception of any kind. It is a DNS-only abstraction: a Service object whose
spec.type: ExternalNameandspec.externalName: foo.example.comcauses CoreDNS to return a CNAME record when a Pod resolves<service-name>.<namespace>.svc.cluster.local, redirecting the lookup to the configured external hostname (kubernetes.io — type=ExternalName). The Pod then connects directly to whatever the external name resolves to — bypassing the cluster entirely. The intended use case is giving an external dependency an in-cluster Service name: code in the cluster refers todatabase.prod.svc.cluster.localrather than toprod-db.cluster-xyz.us-east-1.rds.amazonaws.com, so that when the dependency is eventually migrated into the cluster you only need to change the Service definition — the consuming code stays put. It is comprehensively misunderstood: engineers routinely expect that “Service” implies “kube-proxy mediates the traffic” and therefore that ExternalName services can rewrite, secure, or filter the connection. They cannot. There is no rewriting, no TLS termination, no NetworkPolicy enforcement, no Ingress-style hostname rewriting. The client Pod’s libc DNS resolver follows the CNAME and connects to the resulting IP on whatever port and protocol the client chose. ExternalName is a thin alias layer, nothing more.
Mental Model
flowchart LR POD[Client Pod<br/>connect to<br/>database.prod.svc.cluster.local:5432] COREDNS[CoreDNS<br/>kubernetes plugin] PUBDNS["Public/External DNS<br/>(8.8.8.8 via forward plugin)"] EXT[prod-db-1.abcdef.us-east-1<br/>.rds.amazonaws.com<br/>10.50.30.45] SVC[Service<br/>name: database<br/>namespace: prod<br/>type: ExternalName<br/>externalName: prod-db-1.abcdef.us-east-1.rds.amazonaws.com] SVC -- "defines CNAME mapping" --> COREDNS POD -- "1. resolve database.prod.svc.cluster.local" --> COREDNS COREDNS -- "2. CNAME prod-db-1.…amazonaws.com" --> POD POD -- "3. resolve prod-db-1.…amazonaws.com" --> COREDNS COREDNS -- "4. not in-cluster, forward upstream" --> PUBDNS PUBDNS -- "5. A 10.50.30.45" --> COREDNS COREDNS --> POD POD == "6. TCP SYN to 10.50.30.45:5432<br/>(direct; bypasses kube-proxy entirely)" ==> EXT
What the diagram shows. The Service object isn’t a packet-handling component at all — it’s a CNAME record inserted into CoreDNS by the kubernetes plugin. When a Pod resolves the Service’s in-cluster DNS name, CoreDNS returns a CNAME to the external name; the Pod’s resolver then follows the CNAME (usually with a second query to CoreDNS, which forwards via its forward plugin to the cluster’s upstream resolver), gets the external A record, and the Pod opens a TCP/UDP connection directly to the external endpoint. No kube-proxy rule is touched. No EndpointSlice is consulted. No NetworkPolicy at the Service level applies. The insight to extract: an ExternalName Service is a feature of DNS, not of Services in the kube-proxy sense. If your mental model of kind: Service includes “VIP”, “selector”, or “endpoints”, erase it when you see type: ExternalName. None of those things exist for this type.
Mechanical Walk-through
What CoreDNS does
CoreDNS’s kubernetes plugin watches the apiserver for Service objects. For a Service with type: ExternalName, it constructs a CNAME mapping:
<service-name>.<namespace>.svc.cluster.local. 30 IN CNAME <externalName>.
The TTL (default 30 s, configurable in CoreDNS’s Corefile via the ttl directive of the kubernetes plugin) governs how long Pod-local resolver caches hold the CNAME. There is no other in-cluster machinery — no kube-proxy rule, no EndpointSlice, no status.loadBalancer. The Service object exists purely to give CoreDNS something to publish.
When the Pod’s resolver follows the CNAME, the next lookup goes back through CoreDNS for <externalName>. If the external name happens to itself be *.cluster.local (unlikely but legal), CoreDNS resolves it in-cluster; otherwise, the forward plugin in the Corefile forwards the query to the upstream resolver (typically a cloud-VPC resolver or 8.8.8.8), which returns the actual A/AAAA record.
Why “ports don’t really work”
You can put a ports: block on an ExternalName Service. The apiserver accepts it. It has no effect on traffic. The reason is mechanical: CoreDNS publishes a CNAME, not an A record; the client never dials a Service VIP; the client opens a TCP connection on whatever port the client code specifies. Whether the Service’s ports[].port says 80 or 8080 or XYZ does not change anything — the kernel does not consult the Service object when the Pod calls connect(<resolved-IP>, <client-chosen-port>).
The ports: field on an ExternalName Service therefore serves only as documentation / metadata — and even there it tends to mislead readers into thinking the field is enforced. The official docs and most secondary sources are explicit on this point: “ExternalName does not support ports — the client must know the correct port.”
A subtle exception: some SRV-aware clients (rare in practice) consult _port._proto.<service>.<ns>.svc.cluster.local SRV records, which CoreDNS does publish based on the Service’s ports[]. So in theory an SRV-aware client could be steered to a different external port. Almost nothing in practice uses this; the typical HTTP/gRPC/database client does not query SRV.
Why externalName must be a DNS name, not an IP
spec.externalName is validated as an RFC 1123 hostname; raw IP addresses are rejected. The reason is that the field is rendered into a CNAME, and DNS CNAMEs can only point to other names, not to IP addresses (a CNAME-to-IP is invalid per RFC 1035). If you need to alias to a specific IP, you instead use a selectorless Service with a hand-written EndpointSlice containing that IP — different mechanism entirely (it gets a real VIP, kube-proxy rules, and traffic interception). The Service VIP fronting a hand-written endpoint is the right pattern when (a) the dependency is addressed only by IP, or (b) you want NetworkPolicy enforcement, or (c) you want the dependency to appear to be a normal in-cluster Service in every respect except who owns it.
What ExternalName does NOT give you
- No traffic rewriting. The client connects to the external IP with whatever Host header, SNI, and protocol it chose. If the external service expects a specific
Host:header, the client must set it; the Service does nothing. - No TLS termination. The client’s TLS connection terminates at the external endpoint. If the cluster wants to MITM-inspect or re-encrypt, ExternalName is the wrong layer.
- No NetworkPolicy at the Service level. NetworkPolicy applies to Pod-to-Pod traffic (or Pod-to-CIDR egress) and is enforced by the CNI at the source/destination Pod’s network namespace. An ExternalName Service is just a DNS alias; the actual outbound connection is to a public IP, which any egress NetworkPolicy must allow at the IP level, not the Service-name level.
- No source IP rewriting. The Pod connects directly; the external endpoint sees the Pod’s egress IP (after the cluster’s egress NAT, if any).
- No load balancing. If the external CNAME resolves to multiple A records, the Pod’s libc resolver picks one (often the first) and that’s it. No round-robin across the resolved set unless the client implements it.
- No retries, no circuit breaking, no observability. All of the cross-cutting concerns a service mesh or proxy would add must be implemented by the client.
Cross-cluster and cross-namespace aliasing
A common pattern: in cluster A, create database.prod.svc.cluster.local of type: ExternalName, externalName: external-db.example.com. When the database is later migrated into the cluster (operator-installed Postgres, say), the engineer changes the Service from type: ExternalName to type: ClusterIP with a selector matching the new Pods. The DNS name stays the same. The migration costs one Service edit and is invisible to consumers — provided they were resolving fresh and not caching DNS aggressively.
Another pattern: cross-namespace aliasing. A Service auth.shared.svc.cluster.local of type: ExternalName, externalName: auth.platform.svc.cluster.local redirects in-cluster traffic from one namespace’s perceived Service name to another’s. Useful for multi-tenancy where tenant namespaces shouldn’t know about platform namespaces directly.
Security: CVE-2021-25737 and friends
ExternalName Services are not “secure by default” against pointing at internal infrastructure. A user with permission to create Services in a namespace can create private-data.foo.svc.cluster.local pointing at internal-admin.example.com, possibly redirecting traffic in confusing ways. A separate but related issue, CVE-2021-25737 (fixed in K8s 1.21.1 et al.), allowed EndpointSlice entries to contain localhost / link-local IPs that the API server’s validation didn’t reject — letting a tenant create a Service pointing at the node’s loopback or metadata service. The fix added validation that rejects loopback / link-local / multicast IPs in Endpoints, EndpointSlices, and Service External IPs. ExternalName itself was not the vector (it’s DNS-only), but the same threat model — “a user with Service-write can confuse traffic destinations” — applies. The mitigation is admission policy (Kyverno, OPA Gatekeeper) that restricts spec.externalName values to an allow-listed suffix list.
Configuration / API Surface
apiVersion: v1
kind: Service
metadata:
name: database # In-cluster name; resolves to database.prod.svc.cluster.local
namespace: prod
spec:
type: ExternalName
externalName: prod-db-1.abc.us-east-1.rds.amazonaws.com # CNAME target
# MUST be a DNS name; RFC 1123 hostname
# MUST NOT be an IP address
# selector — NOT allowed for ExternalName (the apiserver rejects)
# clusterIP — NOT allocated, must be omitted (or set to None)
# ports — accepted but has NO effect on traffic; documentation only
ports:
- name: postgres
protocol: TCP
port: 5432 # Metadata only; client must dial 5432 itselfWhat’s notable:
spec.externalNameis the only required spec field beyondtype. No selector, no ports, no anything else.- No
clusterIPis allocated (no static-band reservation, no dynamic-band slot consumed). The Service uses zero Service-CIDR addresses. - No EndpointSlice is created (the endpoints controller skips ExternalName).
ports[]is the most common source of confusion. Engineers listport: 5432and assume the cluster’s database client will then connect to port 5432 of the external host. That happens only if the client itself supplies port 5432 in its connection string; the Service doesn’t enforce it.- No mutation through the Service. No
targetPort, no rewriting, no header injection. It’s a CNAME.
Failure Modes
“It’s not redirecting my traffic.” The #1 ExternalName misconception, repeated across GitHub issue #5822 on the K8s website repo and on every Kubernetes forum. The user creates an ExternalName pointing at httpbin.org, runs curl my-service.default.svc.cluster.local, and gets a wrong-host error because httpbin.org’s server returns a 404 for Host: my-service.default.svc.cluster.local. The fix is to set the request’s Host: header to httpbin.org — the client must do this; the Service doesn’t.
DNS caching outlasts the migration. The whole point of ExternalName is that you can flip the Service over to ClusterIP (when the dependency migrates in-cluster) and consumers don’t care. But Java apps with networkaddress.cache.ttl=-1 cache the resolved CNAME chain forever, so they keep dialling the old external IP after the migration. Mitigation: lower DNS TTLs in clients, or rolling-restart consumers post-migration.
TLS / SNI mismatch. A Pod connecting to database.prod.svc.cluster.local and presenting that name as the SNI hostname to an external TLS server gets a certificate validation error (the server’s cert is for prod-db-1.abc.us-east-1.rds.amazonaws.com, not the in-cluster name). The client must be configured to override SNI to the external name. This is one reason why ExternalName is less useful than people hope for HTTPS dependencies — every client needs configuration.
Loopback / metadata-service redirection (pre-CVE-2021-25737). Older clusters allowed crafted Services to point at 169.254.169.254 (the cloud metadata service). Fixed in K8s 1.21.1+. Verify that admission policy still restricts dangerous externalName values.
Cross-cluster DNS chains. An ExternalName pointing at another in-cluster Service in a different cluster (via a federated CoreDNS) creates a multi-hop CNAME chain that some resolvers truncate or fail to follow. Test the resolution path with dig +trace from a Pod.
Pod’s resolver doesn’t follow CNAMEs to upstream. A handful of obscure container distributions (musl-libc-based, some non-glibc Alpines pre-3.16) have known DNS behaviour differences around CNAME chains. Symptom: works on Ubuntu, fails on Alpine. Workaround: use a selectorless Service plus EndpointSlice instead.
NetworkPolicy doesn’t apply. A developer adds a NetworkPolicy that allows egress to database.prod.svc.cluster.local (using a label selector or namespaceSelector) and is surprised that the connection still fails. NetworkPolicy operates on IP/CIDR, not on Service names. The fix is to allow egress to the external IP/CIDR, or use a CNI that supports DNS-based egress rules (Cilium FQDN policies — see Cilium).
Alternatives and When to Choose Them
- Selectorless Service + hand-written EndpointSlice — when you need an in-cluster VIP, kube-proxy DNAT, and NetworkPolicy enforcement for an external dependency. The Service has a real ClusterIP and the EndpointSlice names the external IPs explicitly. More machinery, but you get real “Service-ness” — including NetworkPolicy at the destination IP and observable connections through kube-proxy metrics. Choose this when the dependency is addressed only by IP, or when you need policy enforcement.
- CoreDNS rewrite plugin — for arbitrary in-cluster DNS rewrites that aren’t tied to a Service object. More flexible (you can rewrite any pattern) but lives in cluster-level Corefile config, not as a per-namespace Service. Use for cross-cluster aliasing where Service-level granularity isn’t needed.
- Ingress / reverse proxy — when you need to intercept and rewrite traffic to an external dependency (set Host header, terminate TLS, log requests, enforce auth). Deploy a small nginx/Envoy as a ClusterIP Service that proxies to the external endpoint. Heavier, but the right answer when ExternalName’s “no rewriting” is a deal-breaker.
- External-DNS operator (external-dns project) — solves the outbound DNS problem: publishing in-cluster Services as records in an external DNS zone. The mirror image of ExternalName, not a replacement.
- Service mesh egress gateway — Istio’s
ServiceEntryresource is the Service-mesh equivalent of ExternalName, with policy enforcement, mTLS upgrade to the external endpoint, and observability. Heavier infrastructure but the right answer when “managed view of external dependencies” is a platform requirement. See Istio and Service Mesh System Design. - Direct external addressing — for one-off use cases, just put the external hostname in the client’s config. The ExternalName indirection is only worth doing if the migration-to-in-cluster path is real.
Production Notes
- Don’t reach for ExternalName by default for external dependencies. It is the minimum-viable abstraction — a CNAME. If you want policy, observability, retries, or any cross-cutting concern, ExternalName is the wrong tool. Most production-grade deployments use either a selectorless Service (when IP-level NetworkPolicy is needed) or a mesh ServiceEntry (when L7 cross-cutting concerns matter).
- Watch for engineers cargo-culting
ports:blocks. Emptyports:blocks on ExternalName services proliferate in copy-paste; they’re harmless but they entrench the wrong mental model. Review for clarity. - TLS / SNI overrides are a per-client problem. Document the SNI/Host-header configuration alongside the Service. Pinning the in-cluster name as
Host:is wrong; the external hostname must be passed. - Restrict
externalNamevalues via admission policy. A simple Kyverno or OPA Gatekeeper rule that requiresspec.externalNameto end with a corporate domain (e.g.*.example.com) prevents accidental — or deliberate — redirection to arbitrary internet endpoints. This is good hygiene even when the cluster trusts its tenants. - Beware Java/JVM DNS caching. The JVM’s default DNS cache is forever (
-1) on some distributions. A migration from ExternalName to ClusterIP requires either a JVM-cache-aware deployment or a rolling restart of consumers post-migration. - CoreDNS Corefile TTL. The default 30-second TTL on the Kubernetes plugin can be too long for migration scenarios. Lower it temporarily (10 s) during a planned dependency migration, then restore.
- Cross-cluster federation. Multi-cluster service discovery projects (Submariner, Cilium Cluster Mesh, KubeFed remnants) generally do not use ExternalName as the federation primitive — they use either a synthetic Service with imported endpoints or a mesh-native equivalent. ExternalName’s limitations make it unsuitable for the cross-cluster case.
- DNS-debugging mantra. When ExternalName misbehaves, the answer is always to
kubectl execinto a Pod and rundig <service>.<ns>.svc.cluster.localanddig <externalName>andgetent hosts <service>.<ns>.svc.cluster.local. The Service is just DNS; debug it as DNS.
See Also
- Service (Kubernetes) — the umbrella resource note
- ClusterIP Service — the default Service type; the one with a VIP
- Headless Service —
clusterIP: None; another DNS-driven Service mode - CoreDNS — the cluster DNS server that publishes the CNAME record
- Kubernetes DNS Specification — the contract CoreDNS implements
- EndpointSlice — used by selectorless Services to alias arbitrary IPs (the IP-based alternative)
- NetworkPolicy — does NOT apply to ExternalName traffic at the Service level
- Ingress — for traffic into the cluster; not a substitute for ExternalName
- Cilium — CNI with FQDN-based egress policy (the policy that would catch ExternalName traffic)
- Istio / Service Mesh System Design — mesh-native equivalent (
ServiceEntry) with full policy - Kubernetes RBAC — controls who can create ExternalName Services
- Kubernetes MOC — parent MOC