Skip to main content

"K3s Ignores NetworkPolicy" Was a Myth. Traffic Still Leaked Through — the ServiceLB Blind Spot

We Wrote a deny-all NetworkPolicy, and External Traffic Still Got Through Normally

section01

To bring a test environment closer to production, we applied a deny-all ingress policy with podSelector: {} to a namespace. Pod-to-pod traffic was blocked exactly as intended — a curl from another namespace timed out, as expected. But when we ran the same connectivity check from outside the cluster against a Service exposed as type: LoadBalancer, the response came back normally, as if the NetworkPolicy didn't exist at all.

The moment you realize k3s networkpolicy isn't behaving as expected, the first thing you're likely to suspect is the common explanation: "K3s's default CNI, Flannel, ignores NetworkPolicy." It's true that plain Flannel by itself has no mechanism to enforce NetworkPolicy. But the conclusion up front is this: that explanation doesn't apply to K3s. K3s ships with a mechanism that enforces NetworkPolicy from the start. So why did the traffic leak? The cause wasn't the NetworkPolicy implementation itself — it was the path taken by ServiceLB, K3s's standard LoadBalancer implementation.

Correcting the Myth That "K3s Ignores NetworkPolicy"

According to K3s's official documentation, K3s bundles kube-router's NetworkPolicy controller as a library, enabled by default, in addition to Flannel. It doesn't use kube-router's other features (such as BGP) — only the controller component that enforces NetworkPolicy is pulled in. In other words, the moment you stand up a K3s cluster, without installing any additional CNI, applying a NetworkPolicy resource actually takes effect. The simplification "K3s = Flannel, therefore NetworkPolicy is ignored" is wrong — and this is in fact an important difference from a plain vanilla Flannel-only setup (for example, installing bare Flannel via kubeadm).

If you want to disable this built-in controller, you need to explicitly pass --disable-network-policy at server startup. K3s's official networking options documentation recommends combining this flag when switching to a CNI with its own policy engine, such as Calico or Cilium, to avoid conflicting with the built-in controller.

In other words, the very fact that the deny-all policy in our test environment was working partially is itself evidence that K3s's built-in controller was functioning correctly. The problem wasn't whether the controller existed — it was somewhere else. The work of checking documentation every time to confirm "what's enabled by default" is, on its own, a quiet operational cost of running a K3s cluster yourself. With a managed K3s such as Kubo, this kind of default-configuration verification can be absorbed as the platform's responsibility.

The Real Culprit Is ServiceLB. The NetworkPolicy API Cannot Block Traffic From a Pod's Own Node

section02

The official Kubernetes NetworkPolicy documentation contains a line that's easy to miss but important: Pods cannot block access to localhost, and they have no way to block traffic originating from their own resident node. This isn't a K3s-specific constraint — it's a permanent characteristic of the Kubernetes NetworkPolicy API itself, and no CNI or policy controller can eliminate it.

The reason this specification tends to surface especially in K3s comes down to how ServiceLB (formerly known as Klipper LB), K3s's standard LoadBalancer implementation, works. When you create a Service of type: LoadBalancer, ServiceLB deploys a DaemonSet Pod on each node, and that Pod writes DNAT rules into the node's own iptables to forward traffic bound for the host port to the Service's ClusterIP (What does ServiceLB (Klipper LB) actually do? · Discussion #9927, Networking Services | K3s).

In other words, traffic that arrives via a LoadBalancer can be classified, from the Pod's perspective, as "traffic forwarded by the node itself." Combined with the "node-resident traffic cannot be blocked" constraint described above, this means ingress deny rules in a NetworkPolicy can fail to work as expected against traffic that arrived externally via a LoadBalancer. In fact, one K3s user reported exactly this scenario in K3s Issue #7624: an ipBlock deny rule meant to block access to a LoadBalancer Service had no effect. The same setup reportedly did block traffic as intended on AWS EKS, which supports the idea that the LoadBalancer implementation itself affects whether NetworkPolicy is actually enforced.

Steps to Detect This

section03

This kind of problem shows up as perfectly normal in kubectl get networkpolicy, and apply succeeds without any error. It's the quiet leaking that makes it tricky. The following steps make it easier to notice.

Check Pod-to-Pod Traffic and LoadBalancer Traffic Separately

  • From another Pod in the same cluster, access the target Pod directly and confirm it's blocked by the NetworkPolicy
  • From outside the cluster, via the LoadBalancer Service, access the same Pod and compare whether it's blocked
  • If only the latter gets through, the cause is unlikely to be a NetworkPolicy misconfiguration — it's more likely that the path itself falls outside the scope of policy evaluation

Check the iptables Rules on the Node

The NetworkPolicy-related iptables rules written by kube-router and the DNAT rules written by ServiceLB live in separate chains. Running iptables-save on the target node and checking both the KUBE-ROUTER-prefixed chains and the ServiceLB DNAT rules (managed by the svc--prefixed Pods) lets you trace which path the traffic is actually taking. According to kube-router's official troubleshooting guide, traffic rejected by NetworkPolicy is logged via iptables NFLOG group 100 — comparing whether a rejection is actually being logged, versus traffic getting through with no rejection logged, is another way to separate the two paths.

Fixes — Enforce at L7, or Switch to a Different CNI

section04

Option 1: Don't rely on L4 NetworkPolicy — control access at L7 Using an Ingress controller's or a service mesh's access-control features gives you more flexible restriction by source IP or path. This lets you enforce access control on externally-exposed endpoints while sidestepping the node-resident traffic limitation.

Option 2: Switch to a CNI with more advanced NetworkPolicy capabilities Switching to a CNI with a more advanced policy engine, such as Calico's GlobalNetworkPolicy, is another option. In that case, explicitly pass --flannel-backend=none --disable-network-policy so it doesn't conflict with the built-in kube-router controller (Issue #1308).

There's a caveat when switching, though. Disabling the network policy controller does not automatically clean up the iptables rules kube-router had already written (Issue #7244). If old rules remain in place while the new CNI's rules are added on top, unintended traffic behavior can result. Running k3s-killall.sh, or manually cleaning up with iptables-save/iptables-restore, must be built into the switchover procedure. Implementation details of kube-router's NetworkPolicy controller are documented in the official kube-router user guide, and the basic approach to running NetworkPolicy on K3s is summarized in the official SUSE/Rancher blog.

Establishing this kind of CNI switchover procedure from scratch for every individual cluster, including verifying leftover iptables rules, is operationally costly. Kubo Cloud provides CNI configuration and NetworkPolicy enforcement status visualized through Captain UI, so you don't have to verify these switchover decisions from zero every time.

Conclusion — Verify the Assumption of "Secure by Default," Path by Path

K3s doesn't ignore NetworkPolicy. If anything, thanks to its built-in kube-router controller, it's one of the few distributions where NetworkPolicy works with no additional setup. But "NetworkPolicy is enabled" and "every traffic path is subject to it" are two different things. In this case, the fact that K3s's standard ServiceLB forwards traffic through the node's iptables, combined with the NetworkPolicy API's permanent inability to block node-resident traffic, meant that traffic to an externally-exposed service was quietly leaking through.

This kind of "gap between paths" tends to appear more often the more infrastructure you assemble yourself. Kubo aims to provide K3s-based managed Kubernetes where operations teams don't need to individually verify CNI configuration and NetworkPolicy enforcement coverage. For environments with strict data sovereignty or network isolation requirements, Kubo On-Premise lets you use the same verified configuration on your own infrastructure. The habit of questioning "secure by default" assumptions, path by path, is a useful mindset across Kubernetes operations generally, not just K3s. If you'd like to reduce the cost of verifying your network configuration, feel free to reach out via Contact Us.

Related articles

← Back to all posts