Kubernetes 1.40 does two things to kube-proxy in the same release. KEP-5495 flips the KubeProxyIPVS feature gate to Default: false, so IPVS mode stops starting unless you override it, and KEP-5343 makes nftables the default proxy mode, so any cluster that never set mode: gets moved underneath it. If you run IPVS today on a cluster with LoadBalancers, a NodeLocal DNSCache, and an alert rule someone wrote in 2021, the upgrade picks your dataplane for you.
These eleven tips are the behaviour differences that show up in production rather than in the release notes, each with the command that finds it before the rollout does.
The 11 traps
- Settle three inputs before you touch a ConfigMap, then run the decision per node pool. Kernel version, whether your CNI already replaces kube-proxy, and how many Services you run determine the path, and they often differ between pools. If you are on Cilium with
kubeProxyReplacementenabled there is nothing to migrate, though the upgrade itself has its own set of traps before BGP drops.
The "bump the node image" branch is a separate project with its own failure modes, including kubelet refusing to start with "not run on a host using cgroup v1" on a newer base image. Budget it as one.
- Price the escape hatch:
KubeProxyIPVS=truebuys you exactly three releases. KEP-5495 lays out the schedule: 1.35 logs a deprecation warning, 1.37 introduces the gate atDefault: true, 1.40 flips it tofalseand kube-proxy exits with an error unless you set it back, 1.43 locks the gate and deletespkg/proxy/ipvs, 1.46 removes the gate entirely. Setting it in 1.40 is a legitimate stall with a hard expiry on the 1.43 upgrade, and there is no further extension to negotiate for. - Write
mode:explicitly this week, even if you plan to stay on iptables. KEP-5343 states the failure directly: a cluster on a kernel too old for nftables that upgrades to 1.40 "without having explicitly setmode: iptablesin their config" will start kube-proxy in nftables mode and fail. One line of config removes an entire class of upgrade-day incident, which is cheap next to the ones you find out about from a pager.
# kube-system/kube-proxy ConfigMap, config.conf
mode: "nftables" # or "iptables", never blank
- Adding
--proxy-mode=nftablesto the DaemonSet args does nothing. When kube-proxy starts with--config, command-line flags are ignored (kubernetes/kubernetes #98302), and kubeadm's DaemonSet runs--config=/var/lib/kube-proxy/config.conf. Check which one you have, then confirm the mode actually changed from the logs rather than trusting the rollout. Upgrade changes that silently no-op are a recurring shape in this ecosystem, in the same family asunrecognized format int32in 1.34.
kubectl -n kube-system get ds kube-proxy \
-o jsonpath='{.spec.template.spec.containers[0].command}'
kubectl -n kube-system logs ds/kube-proxy | grep -i 'proxier\|nftables'
- Check the kernel on every node before you edit anything, because the ConfigMap is one object for the whole cluster. nftables mode needs kernel 5.13+ and
nft1.0.1+, and kube-proxy refuses to start below that. Sorting the Node objects by kernel version gives you the fleet answer in one command.
kubectl get nodes -o custom-columns=\
NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion | sort -k2
The blind spot: that string is what kubelet saw when it started, so a node that later rebooted into a rollback GRUB entry still reports the newer kernel. Spot-check those with uname -r on the host. nftables.skipKernelVersionCheck is a development option that skips the check without shipping a newer nft, so it papers over the problem instead of solving it.
- NodePort quietly stops answering on every node address except the primary. iptables and IPVS default
nodePortAddressesto all local IPs; nftables defaults toprimary, meaning the node's primary IPv4 and IPv6 per the Node object (PR #123105). A hardware load balancer pointed at a secondary NIC starts failing health checks the moment the DaemonSet rolls, and the Service looks healthy from inside the cluster the entire time. Keep the old behaviour deliberately, or list the CIDRs you actually front:
nodePortAddresses: ["0.0.0.0/0", "::/0"]
127.0.0.1:<nodePort>is gone and will not come back as a default. KEP-3866 dropped loopback NodePort deliberately to avoid theroute_localnetsysctl, which is the CVE-2020-8558 surface. KEP-6032 adds it back as an opt-in userspace TCP proxy behindKubeProxyNFTablesLocalhostNodePorts: TCP only, rejecting UDP and SCTP, and only when loopback appears innodePortAddresses. Find the callers before the rollout by grepping host-network manifests, systemd units, and monitoring configs for127.0.0.1:3[0-2][0-9][0-9][0-9]. The same inventory is worth having anyway when you get around to default-deny egress.- Expect intermittent "connection reset by peer" on long-lived connections, and answer it with
tcpBeLiberal. iptables mode installs a DROP rule for packets that conntrack marks INVALID, and PR #120412 made that rule conditional so kube-proxy skips it when--conntrack-tcp-be-liberalis set. nftables mode never installs the rule at all, which means the resets described in issue #117924 can resurface on connections that idle through a conntrack window. Watch the per-CPU counters on a canary node first, then set the option:
conntrack -S | tr ' ' '\n' | grep invalid
conntrack:
tcpBeLiberal: true
- Your kube-proxy failure alert stops firing without ever going red.
kubeproxy_sync_proxy_rules_iptables_restore_failures_totalis exported in iptables and IPVS modes only; nftables mode exportskubeproxy_sync_proxy_rules_nftables_sync_failures_totalunder a different name. A counter alert on a metric that stopped existing stays silent forever, which is worse than no alert because the dashboard still looks green. Rewrite the rule ahead of the rollout and pair it with an absence check so a future rename pages you:
absent(kubeproxy_sync_proxy_rules_nftables_sync_failures_total) == 1
Metrics are on :10249/metrics, health on :10256/healthz.
- Scale-test at your real Service count, because nftables has a reported cliff. kubernetes/kubernetes #135639, opened 2025-12-06 against 1.32 to 1.34 on Amazon Linux 2 and 2023 and Bottlerocket, reports sync times of
13m15.34210662sat 1000+ Services alongside kernel soft lockups (watchdog: BUG: soft lockup - CPU#13 stuck for 22s! [nft:222752]). Check the current state of that issue against your exact patch version, then measure rather than trust either side of the argument: record the p99 ofkubeproxy_sync_proxy_rules_duration_secondson IPVS as your baseline and compare it on the canary pool under the same Service count. - Leave your NodeLocal DNSCache configuration exactly as it is. The Kubernetes docs give two recipes: under IPVS the node-local-dns pod listens only on the link-local address and kubelet's
--cluster-dnspoints there, because the CoreDNS ClusterIP is already claimed by thekube-ipvs0dummy interface. Under iptables and nftables the pod can additionally bind the ClusterIP. The IPVS-flavoured setup keeps working after the switch, and tidying it up means changing--cluster-dnson every kubelet plus a restart, which is a second and larger migration hiding inside the first. Give it its own change ticket, after the dataplane is stable.
Wrap-up
The objection you will actually have to argue is the scheduler one. KEP-5495 points out that many IPVS users have not realised the schedulers "aren't actually useful in Kubernetes": every node keeps its own IPVS table, so lc counts connections on that node alone, and conntrack pins every subsequent packet of a flow regardless. A team that says "we need least-connection balancing" has been getting a per-node approximation of random for years. That is a real cost of leaving IPVS only if you run one node.
Verify the switch instead of assuming it. After the canary rolls, ipvsadm -Ln returns nothing, ip link show kube-ipvs0 reports the device is gone, and nft list table ip kube-proxy prints a real ruleset. Hold the canary through one full traffic peak before touching the rest of the fleet, because the differences that hurt appear under load from a client nobody remembered.
The habit worth adopting today is tip 3: pin mode: explicitly in every cluster you own this week. It costs one line per cluster, and it decides whether you choose your dataplane or 1.40 chooses it for you.
Comments
Be the first to comment.