Skip to main content

Service ExternalIPs Are Going Away: The v1.36 Deprecation Timeline and Your Migration Runbook

8 min readDora NodaDora Noda
Share
On this page

If any Service in your fleet still sets spec.externalIPs, Kubernetes has started a countdown you cannot snooze. On May 14, 2026, SIG Network formally deprecated the field in Kubernetes 1.36: every create or update of such a Service now emits a deprecation warning, and the removal path runs through kube-proxy itself. This is not a "we may remove it someday" deprecation — it ends with conforming clusters being required not to support the field at all.

Here is the short version up front. The timeline, straight from the deprecation announcement by Adrian Moisey and Dan Winship:

ReleaseWhat happens to spec.externalIPs
v1.36 (now)Field deprecated; API warnings on every use
v1.40 at the earliestkube-proxy stops honoring the field, with an opt-back-in for stragglers
v1.43 at the earliestSupport removed completely; no opt-back-in, and conformance requires not supporting it

And here is the migration map — each way teams use ExternalIPs today, and where it goes. The rest of this post is the evidence for every row.

What you use ExternalIPs forMove toWhy
Exposing tenant HTTP(S) servicesGateway API routes on an admin-managed GatewayL7 routing with RBAC where it belongs: admins own the addresses
Exposing TCP/UDP services on owned hardwaretype: LoadBalancer via your cloud CCM or MetalLBController-assigned IPs; no two Services can claim the same one
Quick exposed port for debugging or testsNodePortNo hand-pinned IPs at all
A specific pinned IP you "just need to keep"MetalLB honoring the requested IP, or a CCM annotationKeeps the address without the self-service hijack primitive

Why it's going away (in one paragraph)​

spec.externalIPs was an early attempt to give non-cloud clusters something shaped like a cloud load balancer: list any IP in the field, and kube-proxy programs every node to answer for it. The problem is that the API assumes every user in the cluster is fully trusted. CVE-2020-8554 — medium severity, CVSS 6.3, disclosed December 2020 — showed that any principal who can create a ClusterIP Service and set the field can intercept traffic destined for that IP: a textbook man-in-the-middle, reachable with nothing but ordinary Service-write RBAC.

There is no patch and never will be, because the vulnerability is the feature's design. Since Kubernetes 1.21 the project has recommended disabling the field outright, shipping the DenyServiceExternalIPs admission controller for exactly that — but SIG Network judged that flipping it on by default was too large a breaking change. Five years later, the patience ran out: the project is "increasingly unhappy with the insecure by default state of the feature," in the announcement's words, and the alternatives below have matured enough that there is no longer a good excuse.

What is NOT deprecated​

Before you grep your fleet in a panic, note that the phrase "external IP" is overloaded in Kubernetes, and only one of the three meanings is going away:

Where you see itMeaningAffected?
Service.spec.externalIPsHand-assigned IPs kube-proxy answers forYes — deprecated
Node.status.addresses of type ExternalIPThe node's own addressNo
kubectl get svc column EXTERNAL-IP on LoadBalancer ServicesThe provisioned load-balancer addressNo

If you never set the externalIPs field in a Service spec, the deprecation does not apply to you. (You may still want the admission controller from the last section as a precaution against future use — more on that below.)

Audit: find every usage in your fleet​

The audit is one command per cluster. Run it against every cluster you operate — on a Cluster API fleet that means iterating kubeconfigs, not checking the management cluster and calling it done:

bash
kubectl get svc -A -o json | jq -r '
  .items[]
  | select(.spec.externalIPs != null and (.spec.externalIPs | length > 0))
  | "\(.metadata.namespace)/\(.metadata.name): \(.spec.externalIPs | join(","))"
'

Empty output means you are already clean — skip to the admission-controller step at the end and lock the door behind you. Non-empty output is your migration backlog: each line is a Service that will silently stop receiving its external traffic once kube-proxy drops support. Note the failure mode carefully. This is not an API rejection on upgrade day; the field will sit inert in the spec while the dataplane rules that made it work disappear. Services will look configured and simply go dark — which is exactly why you want the inventory before v1.40, not a scramble after.

Check managed guidance for your platform too. Amazon EKS, for example, tells customers directly to migrate ExternalIPs users to LoadBalancer Services, NodePort, or Gateway API, tracking the work under KEP-5707. If your fleet mixes self-managed and managed clusters, the managed side gets the same deadline.

Pick the replacement: a decision table​

The announcement lays out three alternatives, ordered here from "do this" to "please don't":

Gateway API routes (the L7 answer). A cluster admin defines a Gateway with its addresses in .spec.addresses; tenants attach HTTPRoutes to it. Because Gateways are designed to be admin-managed resources, RBAC can reserve address assignment for privileged users while tenants self-serve routes — the exact separation ExternalIPs never had. Gateway API graduated to GA (v1.0) back in October 2023, shipped v1.5 on a new release-train model in February 2026, and inherited the ecosystem's momentum when community Ingress-NGINX reached its announced retirement in March 2026. If the thing behind your ExternalIPs speaks HTTP, this is where it should live.

A load-balancer controller (the L4 answer). On a cloud substrate this is simply type: LoadBalancer reconciled by your cloud-controller-manager — no hand-pinned IPs anywhere. On bare metal or mixed hardware where no cloud API exists, run MetalLB: the admin configures an IP pool, the controller assigns addresses, and it guarantees two Services can never claim the same IP. MetalLB even honors a requested address for backward compatibility with the ExternalIPs workflow, so a pinned IP you genuinely need to keep survives the move. Either way, address allocation moves from "any Service author" to "the controller, under admin-configured policy" — which is the whole security fix.

NodePort (the simple answer). If the consumer is internal tooling, CI, or a debug path that never needed a stable IP in the first place, a NodePort exposes the port on every node with no address management at all. EKS lists it alongside the other two for a reason: the cheapest migration is the one that deletes the requirement.

Hand-managed LoadBalancer status (the officially-discouraged answer). The announcement documents this one mainly to warn you off it: create a type: LoadBalancer Service with a bogus loadBalancerClass so no controller touches it, then hand-write the IP into .status.loadBalancer as a second step. It is marginally better than ExternalIPs — .status is not editable by ordinary RBAC principals — but anyone granted that power can replicate CVE-2020-8554 exactly as before, with no controller checking for collisions. Treat it as a weekend bridge during a migration window, never as the destination.

The self-hosted angle: you own the load balancer​

Here is the uncomfortable part for fleets like ours. ExternalIPs survived longest in self-hosted environments precisely because "no cloud API to provision a load balancer" made it the path of least resistance. But that excuse has expired in both directions.

If your nodes run on Hetzner Cloud, the hcloud cloud-controller-manager already turns every type: LoadBalancer Service into a real Hetzner Load Balancer — there was never a reason to hand-pin IPs on that substrate. If you run bare metal through Hetzner Robot, the community syself fork extends CCM coverage to dedicated servers, and MetalLB covers whatever the CCM cannot reach. And for L7, Gateway API implementations run identically on owned hardware and hyperscalers. A self-hosted PaaS that owns its load balancers, its address pools, and its Gateway classes has less excuse than anyone to still let tenants self-assign external IPs — every layer of the replacement is already under your control.

So the end state for a self-hosted fleet is concrete: CCM or MetalLB owns every L4 address, Gateway API owns every L7 route, and the API server refuses the old field entirely. That last part is one flag on the API server, available since 1.21 and codified as CIS Benchmark item 1.2.3:

bash
kube-apiserver --enable-admission-plugins=...,DenyServiceExternalIPs

Enable it after the migration, and any manifest — yours, a tenant's, a Helm chart you haven't audited — that tries to reintroduce the field fails closed at admission time instead of silently depending on a dataplane feature with a published removal date.

Runbook checklist​

Mapped to the timeline, in order:

  1. Now (v1.36): Run the audit command against every cluster in the fleet. Each hit is a migration ticket.
  2. Now: Triage each hit through the decision table — Gateway API for HTTP, CCM/MetalLB LoadBalancer for TCP/UDP, NodePort for internal-only.
  3. Before v1.40: Complete the migrations. After this line, kube-proxy stops honoring the field by default and unmigrated Services go dark (an opt-back-in exists, but treat it as incident response, not a plan).
  4. After migration: Enable DenyServiceExternalIPs on every API server so the field can never come back.
  5. Before v1.43: Verify no manifests, charts, or GitOps sources still render the field. At this line support is removed entirely and conformance forbids it — there is no flag left to save you.

The deprecation warnings in v1.36 are the polite phase. Use it.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex