Calico is actually a network policy enforcer first and a CNI second, which fundamentally changes how you think about Kubernetes networking.

Let’s get Calico running on K3s, replacing the default Flannel CNI. This isn’t just swapping out a component; it’s about moving from a simple overlay network to a robust, policy-driven routing solution.

First, we need to uninstall Flannel. K3s ships with Flannel by default, and you can’t have two CNIs active.

k3s-uninstall.sh

Then, we’ll install K3s without Flannel, and explicitly tell it to use Calico. This is done via an environment variable or a command-line flag during installation.

# Option 1: Using environment variable
export INSTALL_K3S_EXEC="--disable traefik --flannel-backend=none --disable-network-policy"
curl -sfL https://get.k3s.io | sh -

# Option 2: Using command-line flag
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik --flannel-backend=none --disable-network-policy" sh -

The --flannel-backend=none is crucial here. It tells K3s not to install Flannel. We also disable Traefik (--disable traefik) because we’re focusing solely on networking. --disable-network-policy is a temporary measure; we’ll enable it later once Calico is fully set up.

Now, K3s is running, but it’s not actually using Calico yet. K3s needs the Calico CNI binaries and configuration. The standard way to install Calico for Kubernetes involves applying YAML manifests to the cluster. Since K3s doesn’t have Tiller or Helm installed by default, we’ll use kubectl.

First, grab the Calico manifest. For a standard installation, you’ll want the tigera-operator manifest, which will then manage the Calico installation.

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml

This tigera-operator.yaml deploys the Calico Operator, which is a Kubernetes operator responsible for managing the lifecycle of Calico components. It watches for custom resources (like Installation) and ensures the desired state is achieved.

Once the operator is running, we need to tell it to install Calico. This is done by creating a CustomResourceDefinition (CRD) object for the Installation kind.

cat <<EOF | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  # Configures Calico networking.
  calicoNetwork:
    # Note: The ipPools section is required for Calico to function.
    ipPools:
    - blockSize: 26
      cidr: 10.244.0.0/16
      encapsulation: VXLAN
      natOutgoing: Enabled
      nodeSelector: all()
EOF

This Installation manifest is the heart of the Calico configuration.

  • blockSize: 26 means each node will get a /26 subnet from the overall 10.244.0.0/16 pool, which is standard for Calico to assign IPs to pods.
  • cidr: 10.244.0.0/16 defines the overall IP address space for your pods. This must not overlap with your host network or any other cluster network. If your K3s nodes are on 192.168.1.0/24, this pod CIDR is fine.
  • encapsulation: VXLAN is one of the ways Calico can tunnel pod traffic between nodes. VXLAN is a common choice for overlay networks, similar to Flannel, but Calico offers other options like IPIP or direct routing if your network supports it.
  • natOutgoing: Enabled is important for pods to reach external services. It means that traffic originating from a pod and going outside the cluster will be NATted to the node’s IP.
  • nodeSelector: all() ensures this IP pool configuration applies to all nodes in the cluster.

After applying this Installation manifest, the Tigera Operator will notice it and start deploying the necessary Calico components (like calico-node DaemonSets, calico-kube-controllers Deployments, and the Calico CNI binaries themselves).

You can watch the rollout:

kubectl get pods -n calico-system
kubectl get pods -n kube-system

You should see pods coming up in calico-system. The Calico CNI binaries will be placed in /opt/cni/bin on each node, and K3s will automatically pick them up.

Now, we need to re-enable Kubernetes Network Policies. Calico’s primary value proposition is its robust network policy enforcement.

# Get the K3s config file
K3S_CONFIG="/etc/rancher/k3s/config.yaml"

# Add or modify the network-policy option
sudo sed -i '/^kube-apiserver-arg:/a \    network-policy-enabled: true' $K3S_CONFIG
sudo sed -i '/^kube-controller-manager-arg:/a \    network-policy-enabled: true' $K3S_CONFIG

# Restart K3s to apply the changes
sudo systemctl restart k3s

Alternatively, you can pass this during the initial install:

export INSTALL_K3S_EXEC="--disable traefik --flannel-backend=none --network-policy-enabled"
curl -sfL https://get.k3s.io | sh -

The reason we had to restart K3s (or install with the flag) is that the network-policy-enabled flag is an API server and controller manager argument. These components are started once during K3s initialization. Changing them requires a restart or a fresh install. With network policies enabled, Calico can now enforce the policies you define.

The most surprising thing about Calico is that it treats network policy as a first-class citizen, not an afterthought. It’s designed from the ground up to provide fine-grained control over network traffic between pods, namespaces, and even external endpoints, using a declarative YAML syntax that integrates directly with Kubernetes RBAC.

Here’s a simple example of what your cluster looks like now.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: nginx:latest
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: my-app
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {} # Allows from any pod in the same namespace
    ports:
    - protocol: TCP
      port: 80

This deploys an Nginx pod and then a NetworkPolicy that allows ingress traffic on port 80 from any pod within the same namespace. If you didn’t have this policy, and network policies were enforced, the Nginx pod would be unreachable.

The real power of Calico comes into play when you start defining more complex policies, like allowing ingress only from specific namespaces or pods, or restricting egress traffic. For example, to only allow ingress from pods labeled role: frontend:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-specific
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: my-app
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 80

Calico uses the Kubernetes API to watch for NetworkPolicy objects. When it detects one, it configures the underlying network stack (using iptables, eBPF, or other mechanisms depending on the configuration) on each node to enforce these rules. It doesn’t rely on a separate policy agent; the calico-node agent on each node is responsible for translating the policies into actual network traffic filtering.

One key aspect of Calico’s design is its use of the BGP protocol or encapsulation (like VXLAN or IPIP) for pod-to-pod communication. Unlike simpler CNIs that might use a single overlay, Calico can, in its default BGP mode (which requires more setup and specific network infrastructure), route traffic directly between pods on different nodes as if they were on the same L2 network. This provides better performance and avoids the overhead of encapsulation when possible. However, for simpler deployments or when direct routing isn’t feasible, VXLAN or IPIP are excellent alternatives. The encapsulation: VXLAN in our Installation manifest tells Calico to use VXLAN tunnels.

The next step is to explore Calico’s advanced features like GlobalNetworkPolicies or integrating with a service mesh.

Want structured learning?

Take the full K3s course →