Have you ever followed the official documentation step by step to switch K3s networking to Cilium's eBPF-based kube-proxy replacement, only to find that NodePort traffic mysteriously stops getting through? ClusterIP communication works fine, but hitting a NodePort from outside the cluster returns nothing. This article walks through exactly where K3s's defaults diverge from Cilium's generic instructions, based on the specific points where things actually broke.

1. Why Try Cilium's kube-proxy Replacement on K3s
K3s ships with Flannel (CNI), a built-in kube-proxy, and ServiceLB (formerly Klipper LoadBalancer) out of the box. That's exactly what makes K3s attractive — it just works with zero extra configuration. But once a team needs deeper observability, network policy, or performance at production scale, Cilium — a CNCF Graduated project — becomes a natural next step.
Cilium runs inside the kernel via eBPF and performs service load balancing using hash tables instead of the linear rule processing that iptables relies on. This is said to achieve near-O(1) lookup time regardless of how many Services or endpoints exist (The New Stack). Cilium is considered one of the most active projects in the CNCF, behind only Kubernetes and OpenTelemetry, and it achieved CNCF Graduated status in October 2023 (Isovalent's official blog, reported by CloudNativeNow).
Given this, replacing Flannel and the built-in kube-proxy with Cilium's kube-proxy replacement has become a realistic option for K3s users. The problem is that Cilium's official instructions are written for a generic Kubernetes cluster (typically kubeadm-based), and if you leave K3s's own defaults untouched, you end up with a configuration that's "half-duplicated" in a way that causes trouble.

The time it takes to verify a CNI integration like this is never trivial. The fact that a managed option exists to offload that verification cost is worth keeping in mind when evaluating this kind of networking change.
2. Followed the Official Steps, and Only NodePort Broke
Cilium's official K3s installation guide instructs you to bring up the master node like this (Cilium official documentation):
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC='--flannel-backend=none --disable-network-policy' sh -
To replace kube-proxy itself, you add the --disable-kube-proxy flag. The official docs also document a config-file approach:
# /etc/rancher/k3s/config.yaml
flannel-backend: "none"
disable-network-policy: true
disable-kube-proxy: true
This much is standard and shows up in plenty of guides. But after installing Cilium via Helm on top of this configuration, ClusterIP traffic works fine while NodePort access alone fails. Even in Cilium's own issue tracker, there are documented patterns where NodePort stops functioning depending on how an IP address is attached to a network interface (cilium/cilium Issue #12317).
In K3s environments specifically, this symptom tends to stem from three compounding factors.
Factor 1: ServiceLB (formerly Klipper) Is Still Running
K3s ships ServiceLB by default, a lightweight load balancer that deploys a DaemonSet using hostPorts for every LoadBalancer-type Service (SUSE's official K3s networking documentation). Setting --flannel-backend=none or --disable-kube-proxy does not automatically disable ServiceLB. With ServiceLB still active, its hostPort bindings can conflict with Cilium's eBPF load balancing on the same node, preventing traffic from reaching the expected NodePort.
Factor 2: Traefik's Default Setup Depends on ServiceLB
K3s deploys the Traefik ingress controller by default, which exposes a LoadBalancer Service on ports 80 and 443. That LoadBalancer Service's path also depends on ServiceLB by default, so leaving Traefik enabled without resolving Factor 1 first destabilizes Ingress traffic as well.

Factor 3: Missing API Server Connection Info (k8sServiceHost/k8sServicePort)
Once kube-proxy is removed, the cluster also loses the "proxy the API server Service" function that kube-proxy used to provide. Cilium's official documentation explains that k8sServiceHost and k8sServicePort must be explicitly specified so the Cilium agent can still reach the API server (Cilium official: Kubernetes Without kube-proxy). K3s quickstart instructions often omit this setting, leaving the Cilium agent's initialization incomplete and destabilizing Service connectivity across the board.
The same documentation further notes that because kube-proxy's and Cilium's NAT tables are unaware of each other, connections can break if the two run simultaneously even briefly. In a distribution like K3s, which bundles multiple networking components by default, this kind of "temporary coexistence" happens easily.
3. The K3s-Specific Configuration Differences
With these three factors in mind, here's how to configure Cilium's kube-proxy replacement to run stably on K3s.
Master Node Startup Options
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
--flannel-backend=none \
--disable-network-policy \
--disable-kube-proxy \
--disable=servicelb \
--disable=traefik" sh -
The key addition beyond Cilium's official steps is explicitly adding --disable=servicelb. If you plan to keep using Traefik, drop --disable=traefik and re-enable it only after confirming NodePort works as described below — this makes troubleshooting far easier.
Required Cilium Helm Parameters
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=<API_SERVER_IP> \
--set k8sServicePort=6443 \
--set ipam.operator.clusterPoolIPv4PodCIDRList="10.42.0.0/16"
<API_SERVER_IP> should be an API server address reachable from every worker node (the master node's IP, or a load balancer VIP in an HA setup). Don't forget to match K3s's default Pod CIDR (10.42.0.0/16) — a value explicitly documented in Cilium's own K3s installation guide (Cilium official documentation: Installation Using K3s).
Verifying NodePort Connectivity
# Check whether Cilium's service list has registered the NodePort
cilium service list
# Actually hit the NodePort to confirm
curl http://<NODE_IP>:<NODE_PORT>
If cilium service list doesn't show a NodePort entry, ServiceLB's hostPort bindings are likely still conflicting. Run kubectl get pods -n kube-system -l app=svclb to check whether any ServiceLB pods remain.

Once Cilium's Service implementation is stable this way, the same standardized approach to networking can be reproduced in a managed environment built on Pure Kubernetes, such as Kubo Cloud. Kubo is K3s-based while maintaining full standard Kubernetes API compatibility, so CNCF Graduated projects like Cilium can be adopted directly, without lock-in.
4. externalTrafficPolicy and Traefik: What to Watch For
After disabling ServiceLB and Traefik and moving to a Cilium-centric setup, once you reintroduce Ingress traffic, you also need to pay attention to externalTrafficPolicy. Per the official Kubernetes documentation, externalTrafficPolicy: Cluster (the default) can forward traffic to pods on any node in the cluster at the cost of an extra hop, while externalTrafficPolicy: Local forwards only to pods on the same node, preserving the client IP (Kubernetes official: Virtual IPs and Service Proxies).
K3s's official documentation recommends not setting node-external-ip on any node when you need externalTrafficPolicy: Local to preserve the client source IP in NAT environments such as public clouds (SUSE official K3s documentation). This constraint comes from the Kubernetes Service specification itself, so it doesn't go away just because you've switched to Cilium's eBPF load balancer.
Working through these configuration differences one by one takes real time, even for infrastructure engineers experienced with standard on-prem K8s operations. Kubo's Captain UI provides a visualized management console that lets you check the state of ServiceLB and kube-proxy without turning the cluster into a black box.
5. Pre-Production Checklist
Before putting a K3s + Cilium kube-proxy replacement setup into production, check at minimum the following items.
- Is ServiceLB fully disabled? Confirm
kubectl get pods -n kube-system -l app=svclbreturns nothing - Are k8sServiceHost/k8sServicePort set in the Cilium Helm values? Check with
helm get values cilium -n kube-system - Does
cilium service listshow the NodePort entry? - Does the Pod CIDR match K3s's default (10.42.0.0/16) or your custom value?
- Does the intended
externalTrafficPolicy(Cluster/Local) match your Ingress/LoadBalancer setup? - Did kube-proxy and Cilium ever run simultaneously during migration, even briefly? (As Cilium's official documentation warns, this can break connections due to NAT table inconsistencies)
Verifying each of these items one by one is exactly the kind of work that tends to become tribal knowledge. With Kubo's AI-Driven Deployment, this verification and configuration-gap analysis can be handed off to AI, letting you run a full K3s-based Kubernetes cluster starting at roughly ¥48,000 per month.
Summary
Trying to replace kube-proxy with Cilium on K3s can break NodePort due to K3s-specific defaults the official steps don't mention — a lingering ServiceLB, Traefik's dependency on it, and missing k8sServiceHost/k8sServicePort settings. The issue isn't that Cilium's instructions are wrong; it's that K3s's bundled networking components weren't fully disabled. By disabling ServiceLB and Traefik, explicitly setting the required Cilium Helm values, and verifying NodePort connectivity step by step, you can run the same eBPF-based networking on K3s that you'd get on a standard Kubernetes cluster.
In the world of managed Kubernetes, you no longer have to put up with the high cost, complexity, and lock-in of EKS or AKS. With K3s-based Kubo, you can run genuine CNCF projects like Cilium exactly as they're meant to work, at dramatically better cost efficiency.