K3s networking has several layers that are easy to collapse into one diagram: node-to-node connectivity, Pod networking, cluster DNS, HTTP ingress, LoadBalancer Services, and the stable endpoint used by agents to reach an HA control plane.
K3s packages defaults for most of these layers. Flannel provides the default CNI network, CoreDNS provides cluster DNS, Traefik provides ingress, ServiceLB implements LoadBalancer Services, and an embedded network-policy controller can enforce Kubernetes NetworkPolicy.
That convenience is useful only if the team understands which traffic each component owns.
This guide owns the K3s-specific networking model: Flannel, Traefik, ServiceLB, private node traffic, and the difference between application load balancing and the control-plane registration endpoint. It does not re-explain generic Kubernetes Service types or compare Kubernetes Ingress against LoadBalancer Services; those topics already belong to Kubernetes Services Explained: Ingress and Load Balancers and Kubernetes Load Balancer vs Ingress Controller.
K3s networking has four separate traffic paths
A production K3s design becomes easier to reason about when the traffic is split into four responsibilities.
| Traffic path | Main K3s component | Typical purpose |
|---|---|---|
| Node and Pod networking | Flannel or replacement CNI | Pod-to-Pod and cluster network transport |
| Cluster service discovery | CoreDNS + Kubernetes Services | Stable internal names and Service reachability |
| Application ingress | Traefik or replacement ingress/Gateway controller | HTTP/HTTPS routing to workloads |
| External service exposure | ServiceLB or replacement LB controller | Implement LoadBalancer Services |
| HA control-plane registration | External L4 LB, VIP, or stable DNS | Give servers/agents a stable API endpoint |
The last two are frequently confused.
ServiceLB exposes application Services. A fixed registration address exposes the K3s API/control-plane path. They solve different problems.
A production architecture may use both:
Control-plane path Agents / admins ↓ Stable API registration address ↓ K3s server nodes :6443 Application path Internet ↓ Traefik / ServiceLB entry ↓ Kubernetes Services ↓ Application Pods
Keeping these paths separate prevents a common design mistake: using the application ingress layer as if it were automatically the HA endpoint for the Kubernetes API.
Flannel is the default K3s CNI
K3s installs Flannel as its default Container Network Interface implementation.
The default Flannel backend is VXLAN. K3s also supports other Flannel backends including host-gw and wireguard-native, and Flannel can be disabled entirely when another CNI is required.
A simple default topology looks like:
Node A └── Pods: 10.x.x.x ↓ Flannel VXLAN ↓ Node B └── Pods: 10.x.x.x
The CNI is responsible for making Pod networks reachable across nodes. It is not the same layer as Traefik or ServiceLB.
For production, choose the Flannel backend based on the network you actually have.
| Backend | Good fit | Main requirement |
|---|---|---|
vxlan | General cloud-VM clusters | UDP overlay connectivity between nodes |
host-gw | Flat private L2 networks | Direct Layer-2 reachability between nodes |
wireguard-native | Encrypted node-to-node overlay | WireGuard kernel support on all nodes |
none | Custom CNI such as Cilium/Calico/Canal | Team owns replacement CNI setup and policy behavior |
Do not change CNI simply because another implementation has more features. Replacing Flannel changes a core cluster dependency and should solve a specific requirement such as advanced policy, observability, encryption, routing integration, or platform standardization.
Keep Flannel traffic on trusted private networking
The current K3s requirements are explicit about Flannel network exposure.
With the default VXLAN backend, nodes need UDP 8472 connectivity to one another. K3s warns that this port must not be exposed to the public internet.
With WireGuard-native Flannel, the node-to-node overlay uses WireGuard ports instead of VXLAN.
A safer cloud-VM layout is:
Public internet ↓ Only intended public app/API endpoints ↓ Private VM network ├── K3s server nodes ├── K3s agent nodes ├── Flannel traffic ├── kubelet/control traffic └── etcd traffic when applicable
For self-managed K3s on Raff VMs, this is the preferred boundary: public interfaces are for deliberately exposed services; cluster transport stays private wherever possible.
The broader K3s architecture guide owns server/agent topology. This page owns which traffic belongs on that private network.
The API path is separate from the Pod network
All K3s nodes need to reach the K3s server API on TCP 6443.
In a single-server cluster, agents can register directly against that server:
Agent ↓ TCP 6443 K3s server
In an HA cluster, registering every agent to one specific server IP creates an avoidable operational dependency. K3s therefore supports a fixed registration address in front of the server set.
Agents ↓ Stable registration address ↓ ┌──────────┬──────────┬──────────┐ Server 1 Server 2 Server 3
K3s documents several ways to provide that stable endpoint:
- a Layer-4 TCP load balancer;
- round-robin DNS;
- a virtual or elastic IP;
- another stable endpoint appropriate to the environment.
If the same hostname or IP is used for Kubernetes API access, it must be included in the K3s server certificate SAN configuration.
This control-plane load balancer is not ServiceLB.
K3s documentation explicitly separates them:
- external cluster load balancer → fixed registration/API address;
- ServiceLB → controller for Kubernetes
LoadBalancerServices.
That distinction should appear in every production network diagram.
Traefik is the default K3s ingress controller
K3s deploys Traefik by default as its HTTP/HTTPS ingress controller.
Current K3s releases ship Traefik v3. Traefik watches Kubernetes routing resources and forwards requests to the appropriate backend Services.
The typical path is:
Internet ↓ Traefik ↓ Ingress host/path rule ↓ ClusterIP Service ↓ Application Pods
For example:
app.example.com → web Service api.example.com → api Service
The important K3s-specific detail is how Traefik itself becomes reachable.
K3s deploys Traefik behind a LoadBalancer Service using ports 80 and 443. The built-in ServiceLB controller then implements that LoadBalancer Service on eligible cluster nodes.
That means the default public web path is effectively:
Internet ↓ Node address :80/:443 ↓ ServiceLB ↓ Traefik LoadBalancer Service ↓ Traefik Pods ↓ Application Service ↓ Pods
This is why understanding ServiceLB is necessary even when the team thinks it is “only using Traefik.”
Default Traefik uses ports 80 and 443 through ServiceLB
By default, ServiceLB considers all nodes with the required host ports available.
For Traefik, that means ports 80 and 443 can be occupied by the ServiceLB Pods that implement the Traefik LoadBalancer Service.
K3s documentation notes that those ports will therefore not be available to other HostPort or NodePort workloads on the same eligible nodes.
This creates an important production decision:
Which nodes should actually accept public HTTP/HTTPS traffic?
For a small cluster, using all nodes may be acceptable.
For a cluster with dedicated control-plane servers, database-heavy nodes, or private worker pools, it may be better to restrict ServiceLB and Traefik to a selected ingress node set.
A cleaner topology is:
Internet ↓ Ingress node pool ├── Node A :80/:443 └── Node B :80/:443 ↓ Traefik ↓ Private cluster Services ↓ Application Pods on any eligible worker
This keeps the public entry role explicit rather than making every cluster node an application edge by default.
ServiceLB implements Kubernetes LoadBalancer Services without a cloud LB
Upstream Kubernetes defines Service objects with type: LoadBalancer, but Kubernetes itself does not provide a universal external load-balancer implementation.
K3s includes ServiceLB for this job.
For each LoadBalancer Service, ServiceLB creates a DaemonSet in kube-system. The ServiceLB Pods use the Service port as a hostPort on eligible nodes.
Conceptually:
LoadBalancer Service :443 ↓ ServiceLB controller ↓ DaemonSet ├── LB pod on Node A :443 ├── LB pod on Node B :443 └── LB pod on Node C :443
A ServiceLB Pod can only run on a node if the required host port is available. If no eligible node has the port available, the LoadBalancer Service remains pending.
This operating model is very different from a managed cloud load balancer that lives outside the worker nodes.
ServiceLB is attractive because it works without requiring an additional cloud-provider integration. The trade-off is that node ports and node reachability become part of the external service path.
Restrict ServiceLB to dedicated nodes when the edge role matters
K3s supports explicit ServiceLB node selection.
When one or more nodes receive the svccontroller.k3s.cattle.io/enablelb=true label, ServiceLB switches into allow-list behavior and only labeled nodes are eligible to host its load-balancer Pods.
K3s also supports ServiceLB pools so different LoadBalancer Services can be assigned to different node groups.
That gives teams three useful patterns:
| Pattern | Best fit | Trade-off |
|---|---|---|
| All nodes eligible | Small/simple clusters | Every suitable node can become public edge |
| Dedicated LB nodes | Clear ingress/security boundary | Extra node-role planning |
| Multiple LB pools | Different public services or network zones | More labels and operating policy |
At Raff, we prefer assigning public responsibility deliberately. If only two nodes should accept web traffic, encode that as architecture instead of relying on every node having ports 80/443 available forever.
ServiceLB, MetalLB, and external load balancers solve different constraints
K3s can use another load-balancer controller instead of ServiceLB. K3s documentation specifically notes that ServiceLB can be disabled when a different controller such as MetalLB is used.
Do not replace ServiceLB merely because MetalLB is popular.
Evaluate the limitation you need to solve:
| Requirement | ServiceLB | Alternative LB controller / external LB |
|---|---|---|
Simple LoadBalancer Services on VMs | Strong fit | Also possible |
| Use node host ports directly | Native behavior | Depends on implementation |
| Advertise dedicated LAN/VIP addresses | Limited by node-address model | Often stronger with MetalLB/Kube-VIP-style design |
| BGP/L2 address advertisement | No | MetalLB-style controller may fit |
| Provider-managed external LB | No | Use provider/infrastructure integration |
| Stable K3s API registration endpoint | Not its role | External L4 LB/VIP fits |
The phrase “K3s load balancer” is ambiguous. Always specify whether you mean:
- control-plane registration load balancer;
- ServiceLB for application
LoadBalancerServices; - Traefik ingress traffic; or
- a third-party load-balancer controller.
That one naming habit eliminates a surprising amount of networking confusion.
Do not confuse Ingress with the HA API load balancer
Traefik Ingress routes application HTTP/HTTPS traffic.
The HA registration endpoint routes K3s API traffic to server nodes.
They are not interchangeable.
Application traffic HTTPS :443 ↓ Traefik ↓ App Services Control-plane traffic Kubernetes API :6443 ↓ HA registration endpoint ↓ K3s server nodes
Running Traefik redundantly does not automatically make the Kubernetes API endpoint highly available.
Likewise, putting HAProxy in front of port 6443 does not expose your application Ingress.
Design, firewall, monitor, and troubleshoot the two paths independently.
CoreDNS owns cluster service discovery
K3s deploys CoreDNS automatically unless it is explicitly disabled.
That gives workloads stable Kubernetes DNS names for Services and other supported cluster records.
A typical internal path is:
web Pod ↓ DNS api.default.svc.cluster.local ↓ ClusterIP Service ↓ api Pods
CoreDNS is part of the cluster network dependency chain, but it should not be confused with the external DNS that points app.example.com to the public application entry.
Use different ownership language:
- external DNS → how users find the public K3s application endpoint;
- CoreDNS → how workloads discover cluster Services internally.
If cluster DNS is failing, adding another ingress controller will not fix Service discovery.
NetworkPolicy is separate from reachability
K3s includes an embedded network-policy controller based on kube-router's network-policy implementation.
Flannel makes Pod connectivity possible. Kubernetes Services make workloads addressable. NetworkPolicy decides which selected workload paths should be allowed or denied when policy is defined.
These are separate controls:
CNI = Can packets be routed? Service / DNS = How is the workload addressed? NetworkPolicy = Is this traffic permitted?
A private cluster network should not be interpreted as permission for every Pod to communicate with every sensitive workload forever.
For example:
Ingress controller ↓ allowed web Pods ↓ allowed api Pods ↓ allowed postgres random namespace ✕ denied to postgres
If a custom CNI provides its own policy engine, plan the policy transition carefully. K3s recommends disabling its embedded network-policy controller when using a CNI whose policy implementation would conflict.
When to keep the K3s defaults
For many small and medium self-managed clusters, the default stack is enough:
Flannel VXLAN + CoreDNS + Traefik + ServiceLB + embedded NetworkPolicy controller
Keep the defaults when:
- private node-to-node connectivity is straightforward;
- VXLAN works correctly across the VM network;
- Traefik supports the required HTTP/HTTPS routing;
- ServiceLB's host-port model fits the public entry design;
- advanced BGP/L2 advertisement is not required;
- the team benefits from fewer cluster components to upgrade.
This is the strongest operational argument for K3s: the packaged network stack can be adequate without assembling five separate projects first.
When to replace Flannel
Replace Flannel only when the CNI requirement is specific.
Examples include:
- an organization standardizes on Cilium or Calico;
- advanced network policy or identity capabilities are required;
- observability requirements depend on a specific dataplane;
- the environment requires routing behavior Flannel does not provide;
- multi-network Pods require a separate architecture;
- platform teams already operate another CNI consistently across clusters.
A custom CNI becomes part of cluster bootstrap, upgrades, incident response, and recovery. Do not treat it as a cosmetic optimization.
K3s supports disabling Flannel with --flannel-backend=none, but the replacement CNI must then provide a complete working Pod network.
When to replace Traefik
Traefik is a strong default when the cluster needs conventional HTTP/HTTPS routing.
Replace it when a named requirement exists, such as:
- an existing platform standard on another ingress/Gateway controller;
- a required feature exists only in another implementation;
- traffic policy must integrate with an established proxy stack;
- a security or operations team already owns another controller;
- the application requires a different protocol/control model.
K3s supports disabling packaged Traefik, but do so consistently across all server nodes and replace its routing responsibility deliberately.
Do not switch to another ingress controller just because the application team already knows Nginx. That familiarity is useful only if it lowers total operating risk.
Gateway API is available through current K3s Traefik
Current K3s networking documentation states that K3s ships Traefik v3 with optional Gateway API support.
Gateway API offers a newer and more expressive Kubernetes traffic model than traditional Ingress. It can become useful when:
- infrastructure and application route ownership must be separated;
- routing policy is becoming more complex;
- new traffic features should avoid controller-specific Ingress annotations;
- the platform is standardizing new clusters around Gateway API.
For a small cluster already working well with standard Ingress, enabling Gateway API does not automatically improve reliability.
This guide therefore treats Gateway API as an optional traffic-model evolution rather than a required K3s networking upgrade.
Cloud VM NAT can affect client-source-IP behavior
K3s ServiceLB behaves differently depending on whether traffic arrives directly at a node address or passes through NAT.
Current K3s documentation includes a specific warning for public-cloud-style NAT: using externalTrafficPolicy=Local for client source-IP preservation can interact incorrectly with configured node-external-ip values in that topology.
The practical lesson is broader than that one setting:
test the actual packet path in the environment where K3s runs.
Do not assume that:
- public node IP equals the address seen inside the VM;
- client IP preservation works automatically;
- ServiceLB behaves like a managed external load balancer;
- firewall rules are identical before and after NAT;
- every node should advertise the same public role.
For production web workloads, verify:
- the public DNS destination;
- the address traffic actually reaches;
- whether NAT occurs;
- which node ports receive traffic;
- where client source IP is preserved or translated;
- what Traefik records in access logs.
This makes source-IP and firewall behavior observable instead of assumed.
Production network architecture should separate node roles
A small all-in-one K3s cluster can legitimately use every node for control plane, workloads, and ingress.
As the cluster becomes more important, explicit roles make failure and exposure easier to reason about.
For example:
Internet ↓ Public ingress nodes Node A Node B ↓ ↓ Traefik ↓ Cluster Services ↓ Private worker nodes ├── app Pods ├── workers └── internal services ↓ private data layer Control-plane registration ↓ Stable :6443 endpoint ↓ Private K3s server nodes
This pattern is not mandatory. It costs more nodes and more role configuration.
Use it when the benefit is real:
- control-plane nodes should not be public web edges;
- security rules differ by node role;
- ingress needs dedicated capacity;
- maintenance should drain application entry separately from the control plane;
- the cluster has enough scale to justify role isolation.
Failure-mode review
A production K3s network should answer what happens when each layer fails.
| Failure | Likely effect | Recovery owner |
|---|---|---|
| One worker node fails | Pods reschedule if capacity/state allows | Kubernetes/workload layer |
| Flannel path fails | Cross-node Pod traffic degrades | CNI/node networking |
| CoreDNS fails | Service-name resolution fails | Cluster DNS |
| One Traefik replica/node fails | HTTP ingress may degrade depending on replicas/edge routing | Ingress layer |
| Port 80/443 unavailable on LB nodes | Traefik ServiceLB placement can fail | Node/ServiceLB configuration |
| ServiceLB controller disabled/misconfigured | LoadBalancer Services may remain pending or lose exposure | K3s networking layer |
| Stable API endpoint fails | Agents/admins may lose control-plane access | HA registration LB/DNS/VIP |
| One K3s server fails | HA API remains if server/datastore quorum and endpoint remain healthy | Control-plane layer |
| External public DNS fails | Users cannot locate application entry | External DNS |
This is why “the cluster network is down” is too broad a diagnosis.
Identify the failed traffic path first.
K3s networking decision framework
Use this baseline for self-managed K3s on cloud VMs:
| Requirement | Recommended starting point |
|---|---|
| Pod-to-Pod networking | Default Flannel VXLAN |
| Encrypted overlay required | Evaluate Flannel WireGuard-native or a custom CNI |
| Flat L2 private network and lower overlay overhead desired | Evaluate host-gw |
| Advanced CNI/policy requirements | Replace Flannel deliberately |
| Internal service discovery | CoreDNS |
| Normal HTTP/HTTPS routing | Packaged Traefik |
Simple LoadBalancer Services on VM nodes | ServiceLB |
| Restrict public edge to selected nodes | ServiceLB node allow-list/pools |
| Dedicated VIP/BGP/L2 addresses | Evaluate MetalLB/Kube-VIP or infrastructure LB |
| HA server registration/API endpoint | Separate L4 LB, VIP, or stable DNS |
| New advanced HTTP routing model | Evaluate Gateway API support in Traefik v3 |
The K3s default stack should remain the baseline until a requirement proves it insufficient.
Raff networking boundary for self-managed K3s
A self-managed K3s cluster on Raff VMs should use private node communication for the cluster plane wherever possible.
A practical topology is:
Raff private cloud network ├── K3s servers ├── K3s agents ├── Flannel traffic ├── etcd/control traffic └── internal application Services Public surface ├── application HTTP/HTTPS through selected ingress nodes └── Kubernetes API only when administrative requirements justify it
Use Private Cloud Networks to separate cluster traffic from unnecessary public paths. Use Raff Kubernetes when the team wants Kubernetes without owning the K3s control-plane and packaged networking lifecycle itself.
At Raff, the design rule is to make node role visible in the network. A VM becoming a Kubernetes node should not automatically mean every Kubernetes and application port becomes publicly reachable.