Migrating Away from Service externalIPs in Kubernetes 1.36
Required knowledge for the CKS certification.
The .spec.externalIPs field on a Service object lets a cluster operator route traffic for an arbitrary external IP into a Service. It was added early in the project to give non-cloud clusters a load-balancer-like capability, but the API does not gate which IP a caller may claim. Any user who can create or update a Service can claim any IP — including IPs already in use by other workloads or by external systems — and steer matching traffic into endpoints they control. That property is the subject of CVE-2020-8554, and it is the reason Kubernetes 1.36 formally deprecates the field.
This article covers what the deprecation does, which alternatives are appropriate, how to migrate an existing Service, and how to block any future use of the field with the DenyServiceExternalIPs admission controller.
1. The Underlying Security Problem
Before: The API server accepts any value in .spec.externalIPs from any user who is authorized to create or update a Service. kube-proxy then programs node-level rules that direct packets destined for those IPs to the Service's endpoints. A user with the ability to create a ClusterIP Service and set .spec.externalIPs can therefore intercept traffic to any IP address they nominate. NVD records CVE-2020-8554 as Medium severity, with two CVSS 3.1 base scores published on the advisory: 5.0 (vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L) from NIST, and 6.3 (vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L) from the Kubernetes CNA. The advisory states the issue affects "Kubernetes API server in all versions."
The kubernetes.io upstream blog announcing the 1.36 deprecation describes the design flaw directly: "the API assumes that every user in the cluster is fully trusted, and in any situation where that is not the case, it enables various security exploits."
After: In Kubernetes 1.36, .spec.externalIPs is formally deprecated. The Service concept documentation now carries a deprecated feature-state banner for the field and instructs operators: "All users should begin migrating away from externalIPs. Consider using an external load balancer controller or a Gateway API implementation instead." The upstream announcement notes that the implementation is expected to be removed from kube-proxy in a future minor release, and that the Kubernetes conformance criteria will be updated to require that conforming implementations do not provide support.
2. What the Deprecation Does (and Does Not) Change in 1.36
The deprecation in 1.36 is a notice — not a runtime behavior change. The field is still accepted by the API server and is still honored by kube-proxy in 1.36. Existing Services that use the field continue to work, and kubectl apply of a manifest that sets the field still succeeds unless an admission controller blocks it.
What the 1.36 release does change:
- The field is now marked deprecated in the Service concept documentation.
- The upstream announcement is the first formal signal of a future removal from kube-proxy and from conformance.
- The
DenyServiceExternalIPsadmission controller, which has been available for several releases, is the recommended way to block any new use of the field while existing Services are migrated.
Treat this release as the migration window. Removal in a future minor release will not include a grace period beyond the deprecation policy itself.
3. Audit Existing Services for .spec.externalIPs
Before changing anything, list every Service in the cluster that uses the field. The following command prints <namespace>/<name> and the IP list for any Service with one or more externalIPs values:
kubectl get svc --all-namespaces \
-o jsonpath='{range .items[?(@.spec.externalIPs)]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.externalIPs}{"\n"}{end}'
For audit reporting, the same data as JSON:
kubectl get svc --all-namespaces -o json | \
jq -r '.items[] | select(.spec.externalIPs != null) |
{namespace: .metadata.namespace, name: .metadata.name, externalIPs: .spec.externalIPs}'
An empty result means the cluster has no Services to migrate, and you can move directly to step 5 to enable DenyServiceExternalIPs as a precaution against reintroduction.
4. Migrate to a Supported Alternative
The upstream guidance is to migrate to "an external load balancer controller or a Gateway API implementation." Pick the alternative that matches the Service's role.
Option A: Replace with a LoadBalancer Service
For Services that fronted application traffic, the direct replacement is a LoadBalancer Service handled by an external load balancer controller (cloud provider, MetalLB, or similar). The IP no longer lives in .spec — it appears in .status.loadBalancer.ingress, which is not editable by ordinary users when RBAC is enabled.
A Service that previously used externalIPs:
apiVersion: v1
kind: Service
metadata:
name: my-example-service
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: my-example-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
externalIPs:
- "192.0.2.4"
Migrated form, with the IP allocation handled by the load-balancer controller:
apiVersion: v1
kind: Service
metadata:
name: my-example-service
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: my-example-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
The controller assigns a load-balancer IP and reports it under .status.loadBalancer.ingress[].ip. The user who creates the Service no longer chooses the IP, which is the property that prevents the CVE-2020-8554 traffic interception.
Option B: Migrate Ingress-Style Routing to the Gateway API
For Services that were really fronting HTTP or TCP routing to multiple backends (a common pattern with externalIPs in older clusters), the Gateway API is the upstream-supported replacement. The Service becomes a backend referenced by a HTTPRoute or TCPRoute, and the Gateway implementation handles the external address. The Service itself does not declare any external IP.
The Gateway API is supported by the official Gateway API implementations and by most service mesh and ingress controllers; the choice depends on the cluster's existing ingress posture.
5. Block New Use with DenyServiceExternalIPs
After the audit shows zero Services using externalIPs, enable the DenyServiceExternalIPs admission controller on the API server to prevent reintroduction.
The kubernetes.io admission controllers reference describes its behavior verbatim: "This admission controller rejects all net-new usage of the Service field externalIPs. This feature is very powerful (allows network traffic interception) and not well controlled by policy. When enabled, users of the cluster may not create new Services which use externalIPs and may not add new values to externalIPs on existing Service objects. Existing uses of externalIPs are not affected, and users may remove values from externalIPs on existing Service objects."
The same page records that DenyServiceExternalIPs is disabled by default in Kubernetes 1.36. It is not in the default enabled plugins list for the release.
Enable the controller
Add DenyServiceExternalIPs to --enable-admission-plugins on each kube-apiserver process. On a kubeadm-managed control plane, edit /etc/kubernetes/manifests/kube-apiserver.yaml and append the controller to the existing list:
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --enable-admission-plugins=NodeRestriction,DenyServiceExternalIPs
# ... other flags ...
The kubelet restarts the static-pod kube-apiserver as soon as the manifest is saved. On clusters managed by other tooling, follow the equivalent procedure (Cluster API, kops, vendor-managed control plane) so the flag survives reconciliation.
Verify the controller is rejecting new uses
Apply a Service that sets the field and confirm the admission controller rejects it:
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: externalip-probe
namespace: default
spec:
type: ClusterIP
selector:
app: nonexistent
ports:
- port: 80
targetPort: 80
externalIPs:
- "192.0.2.4"
EOF
The expected result is a Forbidden error from the API server citing the DenyServiceExternalIPs admission plugin and no Service is created. If the Service is admitted, the flag did not take effect — re-check the manifest edit and the kube-apiserver startup logs for the active --enable-admission-plugins value.
The controller's "existing uses of externalIPs are not affected" semantics mean enabling it during migration does not break in-flight Services. Audit first, migrate next, then enable the controller as a backstop.
6. Verification Step
After migration and admission-controller enablement, re-run the audit command from step 3. The expected output is empty:
kubectl get svc --all-namespaces \
-o jsonpath='{range .items[?(@.spec.externalIPs)]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.externalIPs}{"\n"}{end}'
Combined with the admission-controller probe in step 5, this confirms two distinct guarantees: there are no existing Services exploiting the field, and no new Service can be created with it. CVE-2020-8554's exploit path requires the ability to add an entry to .spec.externalIPs; both checks together remove that path.

References
This article is based on information from the following official sources:
- Kubernetes v1.36: Deprecation and Removal of Service ExternalIPs - Kubernetes Blog
- Service — External IPs - Kubernetes Documentation
- Admission Controllers — DenyServiceExternalIPs - Kubernetes Documentation
- CVE-2020-8554 - National Vulnerability Database