Skip to main content

Kubernetes v1.36 Deprecated Service ExternalIPs: A Bare-Metal PaaS Migration Plan Before v1.40

10 min readDora NodaDora Noda
Share
On this page

Kubernetes v1.36 did not make your Services unreachable. It did something more useful for an operator: it put a dated warning on a networking shortcut that was never an IP-address allocator, never a load balancer, and was unsafe to hand to tenants. If a Service still uses spec.externalIPs, v1.36 now warns on it; the earliest planned kube-proxy disablement is v1.40, and complete removal is no earlier than v1.43. That is enough runway to migrate deliberately — and not enough to leave the job for the next upgrade window.

For a self-hosted PaaS, the first three actions are straightforward: inventory every externalIPs use, stop new ones, and choose the exposure pattern each workload actually needs. Public HTTP applications usually belong behind a shared Gateway; raw TCP or UDP services generally need a LoadBalancer Service with a platform-controlled address. Both need a real way to announce or forward that address to the cluster. externalIPs never supplied that part.

The deadline is real, but v1.36 is the warning phase

The important correction to the usual “v1.36 removed ExternalIPs” headline is that it has not removed the API field or the kube-proxy behavior yet. This change concerns only Service.spec.externalIPs, not the ExternalIP address type on Nodes and not the EXTERNAL-IP column that kubectl shows for a LoadBalancer Service. Kubernetes’ deprecation announcement gives the current migration clock:

StageUpstream behaviorOperator action
v1.36The field is deprecated and its use produces warnings.Find existing users; stop creating new ones.
v1.40 at the earliestkube-proxy support is planned to be disabled, with a temporary opt-back-in.The old path must no longer be part of normal traffic.
v1.43 at the earliestSupport is planned to be disabled completely.Remove the old manifests, exceptions, and rollback dependency.

“At the earliest” matters. An upgrade policy, a managed Kubernetes distribution, or a downstream vendor can change when the behavior reaches a particular fleet; it is not a promise that the cluster gets four years of grace. Treat v1.40 as the latest reasonable readiness target and the temporary opt-back-in as an emergency bridge, not a design.

externalIPs was convenient because kube-proxy installed rules so traffic already arriving at a node with a listed destination IP and Service port could reach endpoints. Kubernetes explicitly leaves allocation and delivery of that IP to the administrator. In other words, it did not bind a new public address to a node or advertise it to the Internet. A routed node address, an upstream NAT rule, or an edge proxy had to exist outside Kubernetes already.

Why this shortcut has to go

The missing ownership boundary is the problem. In a multi-tenant cluster, a user able to create or edit a Service could name an address another workload expects and intercept traffic destined for it. That is the design flaw behind CVE-2020-8554: the advisory notes that a tenant who can create or modify Services and Pods can exploit externalIPs to intercept traffic. There is no ordinary patch that makes tenant-selected addresses safe.

The safe replacement is not “a different field that tenants can choose.” It is a split of responsibility:

  • The platform owns public address pools, BGP or L2 announcements, edge proxies, and Gateway resources.
  • The application owns a Service and, for HTTP(S), an HTTPRoute attached only to a permitted shared Gateway.
  • RBAC and admission policy prevent tenants from bypassing that boundary.

Start with an inventory, including generated Helm output and namespace-specific overlays:

sh
kubectl get services -A -o json \
  | jq -r '.items[] | select((.spec.externalIPs // []) | length > 0)
      | [.metadata.namespace, .metadata.name, .spec.type,
         (.spec.externalIPs | join(","))] | @tsv'

For each row, record the protocol and port, whether the address is dedicated or shared, the DNS and TLS owner, externalTrafficPolicy, the expected client source-IP behavior, and its IPv4/IPv6 requirements. That small worksheet prevents the classic mistake of migrating a TCP endpoint as if it were a hostname-routed website.

Pick the target by traffic and network, not by fashion

MetalLB and Gateway API solve different layers. MetalLB provides address allocation and external announcement for LoadBalancer Services on a cluster without a cloud load-balancer API. Gateway API defines L4/L7 traffic routing and a useful division between operator-owned Gateway infrastructure and application-owned routes. A Gateway implementation commonly exposes itself through a LoadBalancer Service, so a bare-metal deployment may use both.

Existing needNetwork conditionTarget patternConstraint to accept up front
One shared HTTPS entry point for many tenant hostnamesA routable service IP can be advertised or forwarded to the clusterGateway controller + shared Gateway + tenant HTTPRoute; back its Service with MetalLB or an edge load balancerGateway API routes HTTP(S); it does not create a public IP by itself.
Dedicated TCP/UDP endpointAn address pool is reachable on the local L2 networktype: LoadBalancer with MetalLB L2 modeOne elected node receives a given Service IP’s traffic; failover relies on neighbor-cache updates and ingress is bounded by that node.
Dedicated TCP/UDP endpoint at higher scaleThe operator controls or can peer with upstream BGP routerstype: LoadBalancer with MetalLB BGP modeRouter ECMP behavior matters; a node-set change can reset active connections without resilient ECMP.
Public IP cannot be announced from nodesProvider network requires its own forwarding API or an existing edgeKeep the public address at the provider/edge and forward to a controlled LoadBalancer/NodePort gatewayDo not assume L2 or BGP works merely because nodes have public IPs.

This is why “install MetalLB” is not a complete plan. In layer 2 mode, one leader receives traffic for each Service IP, with a single-node bandwidth ceiling and client-dependent failover. In BGP mode, multiple nodes can advertise the service and routers can spread connections, but MetalLB documents that a changed next-hop set normally breaks active connections. For a small L2-connected cluster and modest ingress, L2 is an operationally simple fit. For an operator with a BGP-capable network and a need to spread connections, BGP changes the trade-off — it does not eliminate it.

For tenant web applications, a shared Gateway is usually the cleaner default. The Gateway API model deliberately separates the Gateway an operator runs from the HTTPRoute a developer attaches. It permits hostname, path, header, timeout, and traffic-splitting policy without granting a tenant authority over the public address or every listener on it.

A worked migration: one tenant hostname without a traffic gamble

Suppose orders currently has a ClusterIP Service with externalIPs: [203.0.113.20], and orders.example.com resolves to that address. It serves HTTPS through port 443. The target is a shared Gateway called public-https, exposed by the gateway controller’s LoadBalancer Service. MetalLB receives a reserved address from a platform-owned pool; alternatively, an external edge forwards the address to that Service. Either way, DNS continues to name an address the platform controls.

  1. Freeze and measure. Put a policy exception list under change control, reject all other new externalIPs, and capture current DNS, certificate, HTTP status, latency, source-IP expectations, and long-lived connection behavior. Do not turn on a blanket denial until the inventory is understood.

  2. Prepare the address and data plane. Reserve an IP pool that does not overlap node, pod, or Service CIDRs. Install the selected load-balancer implementation and verify its speaker/controller health. Test a disposable LoadBalancer Service from outside the cluster, then kill its announcing node or BGP session in a maintenance environment. For BGP, verify the router’s ECMP and resilient-hashing behavior; for L2, measure actual failover on representative clients.

  3. Create the platform-owned entry point. The Gateway’s listeners and certificate reference are an operator concern. The route below lets the orders namespace attach only its hostname to that existing entry point:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders
  namespace: orders
spec:
  parentRefs:
    - name: public-https
      namespace: platform-ingress
  hostnames: ["orders.example.com"]
  rules:
    - backendRefs:
        - name: orders
          port: 8080
  1. Prove the new path before the cutover. Test the Gateway IP with the production hostname using a temporary resolver override; verify the TLS certificate, health endpoint, real application path, WebSockets or streaming if used, client-address logging, and a pod/node failure. Watch the HTTPRoute and Gateway status conditions rather than assuming that an accepted manifest means packets flow.

  2. Cut DNS with an explicit rollback boundary. Lower the TTL beforehand, switch orders.example.com only after acceptance checks pass, and leave the old exposure intact for the TTL plus the team’s observed client-cache margin. Roll back DNS while that old path remains healthy if certificates, source IP, failover, or route attachment fails. Do not remove the old Service field merely because a staging curl succeeded.

  3. Close the insecure door. After the last exception is gone, enable the API server’s DenyServiceExternalIPs admission controller. It rejects new Services using the field and additions to existing Services; it is a guardrail, not a migration tool, so apply it after the managed exception list is empty. Removing it is a documented, privileged emergency rollback only — it should not restore traffic paths that a completed migration already deleted.

For a non-HTTP TCP or UDP tenant, skip HTTPRoute and expose a platform-approved LoadBalancer Service instead. Allocate the address from MetalLB’s pool (or the edge provider), restrict who may request it, and run the same external health and failover tests. Shared IPs require a deliberate port-sharing policy; dual-stack requires pools and tests for both families; externalTrafficPolicy: Local must be validated against endpoint placement rather than copied blindly. Those are not implementation details: they decide whether the replacement preserves the old connection contract.

Make the migration a platform capability

The durable result is a small, inspectable public-ingress system rather than a list of special Service fields. Give the platform team write access to Gateway, GatewayClass, address-pool, BGP-peer, and load-balancer configuration. Give tenant namespaces permission to create only HTTPRoute objects whose parent references and hostnames the Gateway permits. Alert on rejected route attachments, address-pool exhaustion, BGP-session changes or L2 leader changes, and every attempted externalIPs admission denial.

Run the work in waves: inventory and freeze now; build the data plane and migrate an internal or low-risk hostname; exercise failover; migrate the remaining HTTP routes and protocol services; then enable the denial controller. Schedule a final audit before the fleet’s first v1.40-or-later upgrade plan. That sequence turns a future upstream removal into a controlled platform improvement, with a tested rollback while it is still safe to use one.

externalIPs was a shortcut around an ingress design. Kubernetes is finally retiring it because that shortcut confused who owns an address. A bare-metal PaaS does not need a cloud load-balancer API to replace it, but it does need to own the missing pieces: IP allocation, announcement or forwarding, routing, RBAC, and failure testing. Doing that while v1.36 only warns is far cheaper than discovering the gap during an upgrade.

Bex.co is an open-source, AI-native Render alternative: push a Git repository and run an HTTPS service on machines you own. Explore the project on GitHub to see the platform model this kind of operator-owned ingress supports.

Sources

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