Kubernetes v1.36 "Haru" shipped on April 22, 2026 with 70 enhancements: 18 graduated to stable, 25 to beta, 25 new in alpha. The headlines went to DRA, OCI volumes, and memory QoS. But the two lines that will actually page a self-hosted fleet operator are buried at the bottom of the release notes under "deprecations": the gitRepo volume driver is permanently removed, and Service.spec.externalIPs is formally deprecated with removal planned.
One of these breaks your workloads the next time a pod restarts. The other starts a countdown of roughly two years. Neither announces itself in CI. Here is what each costs a fleet that still mounts config from git or exposes services on bare-metal node IPs — and exactly what to migrate to.
The two deadlines, up front
| Change | Status in 1.36 | Hard deadline | Failure mode if ignored |
|---|---|---|---|
gitRepo volume (KEP-5040) | Permanently disabled, no re-enable flag | Already here — first pod restart after upgrade | FailedMount: kubelet refuses to mount; pods stay Pending |
Service.spec.externalIPs (KEP-5707) | Deprecated, warnings on use | kube-proxy support off ~v1.40 (opt back in), gone ~v1.43 (no opt-out) | Services silently stop answering on those IPs |
If you run a Cluster-API-managed fleet on owned hardware with no cloud load-balancer API, you are the exact operator both changes are aimed at. The rest of this post is the migration playbook for each.
Removal #1: gitRepo is gone, and your pipeline won't tell you
The gitRepo volume let a pod clone a git repository at startup straight into a mounted volume. It was deprecated in v1.11 — mid-2018 — and survived eight years on inertia. In 1.36 the plugin is permanently disabled and cannot be turned back on. The reason is security, not tidiness: the in-tree driver could let an attacker run code as root on the node, and it carried a symlink-escape flaw (CVE-2024-10220) that let containers traverse out of the cloned tree.
The nasty part is the failure mode. The API server still accepts gitRepo in pod specs — it has to, because old manifests linger in etcd — so helm lint passes, helm template renders, and even kubectl apply --dry-run=server returns success against a 1.36 cluster. CI goes green. Then the kubelet refuses to mount the volume and the pod sits in Pending with gitRepo volume plugin is no longer supported. As one field report put it: your charts pass validation, your pods don't schedule.
Where does gitRepo still hide in 2026 codebases? Three places, roughly in order of embarrassment:
- Internal "config-as-code" sidecars. The pre-2020 pattern: mount a
gitRepovolume so the pod pulls the latest config repo on restart. Most teams replaced it with ConfigMaps or Vault years ago, but forgotten staging environments and legacy clusters still run it. - Helm charts pinned to old versions. Charts published in the 2018–2020 era that were never re-released. The
kubeVersionfield is advisory and almost never gets narrowed for a single volume removal, sohelm upgrade --installhappily rolls forward into a broken combination. - Custom operators that generate pod specs. The operator itself survives the upgrade fine — but the pods it emits fail to mount, and the operator's own readiness probe has no idea its children aren't running. These are the easiest to miss.
The three replacements, compared
Kubernetes points at three supported approaches. They are not interchangeable — pick by refresh semantics:
| Approach | Clone timing | Auth | Refresh | Best for |
|---|---|---|---|---|
Init container + emptyDir | Once, before app starts | Yes, via mounted secret | Pod restart only | Closest to drop-in; static config snapshots |
git-sync sidecar | At start, then periodic | SSH key, PAT, or service account | Configurable interval | Live config repos that change without redeploys |
| GitOps operator (Flux/Argo) | Continuous reconcile | Full secret management | Continuous | Fleets already converging on GitOps anyway |
The init-container route is the smallest diff: an emptyDir the init container clones into and the app container reads from. Note the auth upgrade hiding in this migration — gitRepo never supported credentials, so everything mounted through it was by definition a public repo. If that config was ever sensitive, the migration is the moment to fix that too, not just preserve it.
git-sync is closest in spirit to the old behavior, but it changes pod resource accounting: the extra container can push small pods over their memory limits, so re-check requests and limits when you add it.
Detect it before the upgrade does
Run these before your 1.36 upgrade window, not during it:
# Static manifests, charts, and operator source
grep -rn "gitRepo:" manifests/ charts/ operators/ 2>/dev/null
# Live pods in every cluster (anything returned is a future FailedMount)
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]? | has("gitRepo")) | "\(.metadata.namespace)/\(.metadata.name)"'If you're already on 1.36 and nothing has broken yet, you're flying on workloads that haven't restarted since the upgrade. A node drain or a routine rollout will find them for you.
Deprecation #2: externalIPs gets a countdown to v1.43
Service.spec.externalIPs was Kubernetes' early attempt at cloud-load-balancer-like behavior for clusters without a cloud: declare an IP, and kube-proxy answers on it. Bare-metal fleets adopted it as the poor man's load balancer — expose a service on a node's own routable IP with no LB appliance in the path.
The problem is that the API assumes every user in the cluster is fully trusted. Any user who can create a Service can claim any IP, including IPs belonging to other tenants or to infrastructure, and redirect their traffic — the man-in-the-middle vector documented in CVE-2020-8554. The project has recommended disabling the field since 1.21 and ships a DenyServiceExternalIPs admission controller for exactly that. In 1.36, SIG Network finally started the removal clock, with a dedicated migration post laying out the timeline:
- v1.36 (now): field deprecated; API server emits warnings on use.
- v1.40 at the earliest (~a year out): kube-proxy stops honoring the field by default, with an opt-back-in for stragglers.
- v1.43 at the earliest (~two years out): support removed entirely, no opt-back-in — and conformance criteria will require that conforming implementations do not support it.
One terminology trap to avoid while auditing: this deprecation covers only the Service field. Node .status.addresses entries of type ExternalIP and the EXTERNAL-IP column in kubectl get svc output are unrelated and unaffected.
What replaces it on bare metal
The official post ranks the alternatives, and the ranking matters for a fleet with no cloud LB API:
- A load-balancer controller like MetalLB — the real answer. Install it, carve an
IPAddressPoolfrom your routable range, andtype: LoadBalancerservices get IPs assigned by the controller instead of claimed by users. The admin controls the pool, and the controller guarantees no two services share an IP — which is precisely the CVE-2020-8554 surface, closed. MetalLB even honors the oldloadBalancerIPrequest field, so you can keep existing IPs stable through the migration rather than renumbering every service. On Hetzner specifically, MetalLB's L2 mode is the standard self-hosted answer; the hcloud cloud-controller-manager is the alternative if you're on Hetzner Cloud rather than bare dedicated boxes. - Gateway API — the strategic answer. A
Gatewayresource carries the IP in.spec.addresses, and Gateways are designed to be admin-managed with RBAC keeping tenants out. If your ingress story is heading toward Gateway API anyway, this deprecation is a reason to accelerate, not a reason to build something throwaway. - Manually-managed
LoadBalancerservices — the stopgap. Create the Service with a dummyloadBalancerClassand patch the IP into.status.loadBalancer.ingressas a second step. The IP lives in.statusinstead of.spec, so RBAC keeps ordinary users from editing it. The upstream post calls this "the easiest (but also worst) option" — anyone with status-write permission can still replicate the original exploit, with no duplicate-IP checks. Use it to buy weeks, not years. - NodePort — the honest downgrade. Fine for internal or low-stakes exposure where you never needed a stable IP in the first place.
Detection is one command, and the precaution costs nothing:
# Find every Service still using the field
kubectl get svc -A -o json | jq -r '.items[] | select(.spec.externalIPs) | "\(.metadata.namespace)/\(.metadata.name): \(.spec.externalIPs)"'
# Block new uses now, migrate old ones on your schedule
# Enable the DenyServiceExternalIPs admission controller on the API serverEnabling the admission controller today while grandfathering existing services is the move: it freezes the problem at its current size while you work the migration queue.
The migration order for a fleet operator
The two changes want opposite urgencies, which is why they pair well in one upgrade plan:
gitRepo: fix in this upgrade window. It is a hard break, not a warning. Grep manifests and chart versions, audit operator-emitted pod specs against live clusters, and migrate to init containers orgit-syncbefore the control plane moves to 1.36. Narrow migrated charts'kubeVersionand bump the major version so no one rolls a legacy chart forward onto a new cluster.externalIPs: schedule deliberately, finish before v1.40. You have roughly a year of warnings before kube-proxy flips the default. Stand up MetalLB (or commit to Gateway API), migrate services pool-first with stable IPs vialoadBalancerIP, and enableDenyServiceExternalIPsimmediately so the queue only shrinks.
Doing both on your own schedule beats doing either under a deadline — the first because the deadline is already past, the second because an IP-migration done calmly keeps every address stable, while one done against a kube-proxy default flip does not.
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.



