Skip to content

Flannel How-to Guides

Scope

Task recipes for Flannel v0.28.x: install (manifest, Helm, kubeadm, K3s), pick and change a backend, turn on encryption, dual-stack, nftables and NetworkPolicy, upgrade, migrate away, and troubleshoot. Option tables are in Reference; background is in Explanation.

Prepare Nodes

Flannel needs the br_netfilter kernel module. Since Kubernetes 1.30, kubeadm no longer checks for it, and Flannel will not start correctly without it (README).

# Load now and at every boot
sudo modprobe br_netfilter
echo br_netfilter | sudo tee /etc/modules-load.d/br_netfilter.conf

# Standard CNI plugins (bridge, host-local, portmap) in /opt/cni/bin (version from the upstream README)
ARCH=$(uname -m)
case $ARCH in
  armv7*) ARCH="arm";;
  aarch64) ARCH="arm64";;
  x86_64) ARCH="amd64";;
esac
sudo mkdir -p /opt/cni/bin
curl -O -L https://github.com/containernetworking/plugins/releases/download/v1.7.1/cni-plugins-linux-$ARCH-v1.7.1.tgz
sudo tar -C /opt/cni/bin -xzf cni-plugins-linux-$ARCH-v1.7.1.tgz

Raspberry Pi on Ubuntu 21.10+

VXLAN moved to a separate kernel module package: sudo apt install linux-modules-extra-raspi.

Open the backend port between node IPs (see Reference > Ports). With firewalld:

sudo firewall-cmd --permanent --zone=public --add-port=8472/udp   # VXLAN
sudo firewall-cmd --reload

Install on kubeadm Clusters

Nodes must have a podCIDR. Pass the pod network CIDR to kubeadm so kube-controller-manager allocates one per node:

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

With the Release Manifest

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl -n kube-flannel rollout status ds/kube-flannel-ds

Use the release asset, not the branch copy

Upstream warns that Documentation/kube-flannel.yml on the default branch can lag behind or run ahead of the published images. Applying it can fail with missing binaries (for example install-conf). Pin a release URL such as .../releases/download/v0.28.9/kube-flannel.yml for reproducible installs.

With a Custom Pod CIDR

The manifest hard-codes 10.244.0.0/16. Download it, change Network, and apply:

curl -sLO https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
sed -i 's#10.244.0.0/16#10.100.0.0/16#' kube-flannel.yml
kubectl apply -f kube-flannel.yml

Network must match (or contain) the cluster's --cluster-cidr / --pod-network-cidr.

With Helm

kubectl create ns kube-flannel
kubectl label --overwrite ns kube-flannel pod-security.kubernetes.io/enforce=privileged

helm repo add flannel https://flannel-io.github.io/flannel/
helm install flannel --namespace kube-flannel \
  --set podCidr="10.244.0.0/16" \
  flannel/flannel

The namespace must allow the Pod Security privileged level. baseline rejects the NET_ADMIN/NET_RAW capabilities, hostNetwork and hostPath volumes.

Configure Flannel on K3s

K3s embeds Flannel in the k3s binary, so there is no kube-flannel DaemonSet. Configure it with server flags or /etc/rancher/k3s/config.yaml. Flannel options must be identical on all servers.

# Default: VXLAN, cluster CIDR 10.42.0.0/16, embedded kube-router netpol controller
curl -sfL https://get.k3s.io | sh -

# Encrypted backend
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --flannel-backend=wireguard-native" sh -
# /etc/rancher/k3s/config.yaml (servers)
flannel-backend: host-gw        # vxlan | host-gw | wireguard-native | none
flannel-iface: eth1             # per node; pick the private NIC
cluster-cidr: 10.42.0.0/16,2001:db8:42::/56
service-cidr: 10.43.0.0/16,2001:db8:43::/112
flannel-ipv6-masq: true         # ULA pod ranges need IPv6 NAT

K3s backend notes

--flannel-backend=ipsec and the old wireguard value are deprecated in K3s. Use wireguard-native. Dual-stack must be configured when the cluster is created and cannot be added to an existing IPv4-only cluster (K3s basic network options).

Choose a Backend

Use this decision flow to pick a backend for a new cluster. The backend cannot be changed safely at runtime.

flowchart TD
    A{"Is the underlay untrusted,<br/>or is encryption required?"} -->|yes| WG["wireguard<br/>(K3s: wireguard-native)"]
    A -->|no| B{"Are all nodes on one L2 segment<br/>(bare metal, same VLAN)?"}
    B -->|yes| HG["host-gw<br/>(no encapsulation)"]
    B -->|no| C{"Mixed: some nodes share a subnet,<br/>others are routed?"}
    C -->|yes| VXD["vxlan with DirectRouting: true"]
    C -->|no| VX["vxlan (default)"]
    WG --> W{"Windows nodes?"}
    W -->|yes| VXW["vxlan (VNI 4096+, port 4789)<br/>or host-gw, plus underlay encryption"]

Backend snippets for the Backend object in net-conf.json:

{ "Type": "vxlan", "DirectRouting": true }
{ "Type": "host-gw" }
{ "Type": "wireguard", "PersistentKeepaliveInterval": 25 }

With Helm, set the backend in values instead of editing JSON:

helm upgrade flannel flannel/flannel -n kube-flannel --reuse-values \
  --set flannel.backend=wireguard --set flannel.keepaliveInterval=25

Changing the backend on a live cluster

Upstream says the backend "should not be changed at runtime". Treat it as a maintenance-window change. Update the ConfigMap, restart the DaemonSet, and on each node remove the old tunnel device (for example ip link delete flannel.1) or reboot. Then check that pods on different nodes can reach each other.

Helm tunnelMode rendering

In chart v0.28.9, the template writes "Mode": {{ .Values.flannel.tunnelMode }} without quotes, which produces invalid JSON for values like auto. Keep the default (separate) or check the output with helm template before you apply.

Enable Dual-Stack or IPv6-Only

Prerequisites: each node has IPv4 and IPv6 addresses and default routes on its main interface. The cluster must also be dual-stack, which means dual --cluster-cidr and --service-cidr on the control plane. Only vxlan, wireguard and host-gw support dual-stack.

{
  "Network": "10.244.0.0/16",
  "EnableIPv6": true,
  "IPv6Network": "2001:db8:42::/56",
  "Backend": { "Type": "vxlan" }
}

For IPv6-only, add "EnableIPv4": false and drop Network. With Helm, set podCidrv6 (and podCidr="" for IPv6-only):

helm install flannel flannel/flannel -n kube-flannel \
  --set podCidr="10.244.0.0/16" --set podCidrv6="2001:db8:42::/56"

Check the result on a node. subnet.env should now contain FLANNEL_IPV6_NETWORK and FLANNEL_IPV6_SUBNET:

cat /run/flannel/subnet.env
ip -6 route show | grep flannel

If the pod IPv6 range is publicly routable, route IPv6Network to the cluster from the upstream router. Flannel does not advertise it (issue #2289).

Enable nftables Mode

EnableNFTables is EXPERIMENTAL. It is available since v0.25.0, and iptables remains the default.

# Helm
helm upgrade flannel flannel/flannel -n kube-flannel --reuse-values --set flannel.enableNFTables=true

# Manifest: set "EnableNFTables": true in net-conf.json, then restart
kubectl -n kube-flannel edit cm kube-flannel-cfg
kubectl -n kube-flannel rollout restart ds/kube-flannel-ds

# Verify on a node
sudo nft list tables | grep flannel

Pair it with kube-proxy mode: nftables so the node does not mix iptables and nftables rule sets. Starting Flannel in one mode tries to clean up the other mode's rules (see the nftables ADR).

Enforce NetworkPolicy

Choose one of these options.

# 1. Flannel Helm chart + kube-network-policies (since v0.25.5)
helm upgrade flannel flannel/flannel -n kube-flannel --reuse-values --set netpol.enabled=true
kubectl -n kube-flannel get pods -l app=flannel -o jsonpath='{.items[0].spec.containers[*].name}'

# 2. K3s: already on by default (embedded kube-router netpol). Verify its chains:
sudo iptables-save | grep -c KUBE-ROUTER

The other two options are Canal and Cilium chaining. For Canal, follow Tigera's Install Calico for policy and flannel for networking. On K3s, start with --flannel-backend=none --disable-network-policy and set allow_ip_forwarding: true in the Canal CNI container_settings. For Cilium, see Cilium CNI chaining.

Test that policy is enforced with a default-deny policy:

kubectl create ns np-test
kubectl -n np-test run web --image=nginx --port=80 --expose
kubectl -n np-test apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes: ["Ingress"]
EOF
kubectl -n np-test run client --rm -it --image=busybox --restart=Never -- wget -qO- -T 3 web
# Expect a timeout. If the page loads, nothing is enforcing policy.

Upgrade Flannel

Only the latest release gets security fixes, so upgrade on each patch.

# Manifest installs (from 0.20.2 or newer): apply in place
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

# Helm installs
helm repo update
helm upgrade flannel flannel/flannel -n kube-flannel --set podCidr="10.244.0.0/16"

If labels or selectors changed between versions and the apply fails, upstream's fallback is a clean reinstall. Delete the DaemonSet, ConfigMap, ServiceAccount, ClusterRole/Binding and namespace, then install again and reboot nodes. This causes a cluster-wide network outage (upgrade.md). On K3s, Flannel is upgraded with the k3s binary.

Migrate from Flannel to Cilium or Calico

Migration is disruptive. Plan a maintenance window, and use a new cluster with blue/green cut-over if possible.

K3s:

  1. Reinstall or reconfigure servers with --flannel-backend=none --disable-network-policy. Keep kube-proxy unless the new CNI replaces it.
  2. Install the new CNI (for Cilium: cilium install, or its Helm chart with ipam.operator.clusterPoolIPv4PodCIDRList matching 10.42.0.0/16).
  3. On every node, remove Flannel leftovers and restart: ip link delete flannel.1; ip link delete cni0, and remove /var/lib/rancher/k3s/agent/etc/cni/net.d/10-flannel.conflist if present.
  4. Restart workloads so pods get new-CNI interfaces, then run cilium connectivity test or your own checks.

Manifest or Helm installs (kubeadm):

kubectl delete -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
# or: helm uninstall flannel -n kube-flannel
# then on each node:
sudo ip link delete flannel.1; sudo ip link delete cni0
sudo rm -f /etc/cni/net.d/10-flannel.conflist

For a lower-risk path that only adds policy, keep Flannel and add Canal or kube-network-policies (above). See also CNI Comparison for the comparison-level migration notes.

Troubleshooting

Symptom Diagnose Fix
node <name> pod cidr not assigned kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' Set --pod-network-cidr (kubeadm) or --allocate-node-cidrs --cluster-cidr on kube-controller-manager
failed to read net conf / error parsing subnet config kubectl -n kube-flannel get cm kube-flannel-cfg -o yaml Fix JSON in net-conf.json; check the mount at /etc/kube-flannel/
Cross-node pod traffic fails, same-node works ip -d link show flannel.1, ip route, bridge fdb show dev flannel.1 Open UDP 8472 (or 51820/51821) between nodes; check --iface picks the right NIC
All nodes report the same public IP (Vagrant, multi-NIC) Look for Using interface with name ... in startup logs --iface=eth1, or annotate flannel.alpha.coreos.com/node-public-ip
Nodes behind NAT Peers tunnel to unreachable IPs Set flannel.alpha.coreos.com/public-ip-overwrite (or --flannel-external-ip on K3s)
VXLAN works partially behind NAT, corrupt UDP checksums tcpdump -i <nic> udp port 8472 -vv ethtool -K flannel.1 tx-checksum-ip-generic off (persist via udev rule)
Large packets hang (TLS, image pulls) Compare NIC MTU, FLANNEL_MTU and pod eth0 MTU Fix the underlay MTU or set backend MTU; VXLAN needs NIC MTU minus 50
Flannel pod CrashLoopBackOff on new kernels lsmod | grep br_netfilter modprobe br_netfilter and persist
Error adding route / operation not permitted Check capabilities in the DaemonSet Needs NET_ADMIN, NET_RAW, and hostNetwork; namespace must be PSS privileged
Pods cannot reach services after enabling nftables nft list ruleset, iptables-save Align kube-proxy mode with Flannel's mode; restart both
install-conf binary not found Manifest from default branch Use the release manifest asset

Commands & Recipes

# Flannel pods and logs
kubectl -n kube-flannel get pods -l app=flannel -o wide
kubectl -n kube-flannel logs -l app=flannel -c kube-flannel --tail=100 -f

# Readiness (v0.28.9+, from the node)
curl -s localhost:8081/readyz; curl -s localhost:8081/healthz

# Node subnets and flannel annotations
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
kubectl get node <node> -o jsonpath='{.metadata.annotations}' | tr ',' '\n' | grep flannel

# Subnet file and CNI config on a node
cat /run/flannel/subnet.env
cat /etc/cni/net.d/10-flannel.conflist

# VXLAN dataplane: device, routes, neighbours, FDB
ip -d link show flannel.1
ip route show | grep flannel
ip neigh show dev flannel.1
bridge fdb show dev flannel.1

# WireGuard dataplane
sudo wg show flannel-wg

# Masquerade and forward rules
sudo iptables -t nat -S FLANNEL-POSTRTG
sudo iptables -S FLANNEL-FWD
sudo nft list table ip flannel-ipv4     # nftables mode

# Capture overlay traffic on the underlay NIC
sudo tcpdump -ni eth0 udp port 8472

# Increase log verbosity (manifest installs)
kubectl -n kube-flannel patch ds kube-flannel-ds --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"-v=5"}]'

Sources