Install K3s without the defaults Cilium will replace

curl -sfL https://get.k3s.io | sh -s - \
    --flannel-backend=none \
    --disable-network-policy \
    --disable-kube-proxy \
    --cluster-init

Without a CNI, no pods can communicate — kubectl get pods -A shows the system pods as Pending. That's expected; Cilium provides the CNI.

Install Cilium

# Install the cilium CLI on your workstation
CILIUM_CLI_VER=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --remote-name-all \
    "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VER}/cilium-linux-amd64.tar.gz"
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz

# Install Cilium into the cluster, replacing kube-proxy
cilium install \
    --version v1.16.5 \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost=<api-server-host> \
    --set k8sServicePort=6443 \
    --set hubble.enabled=true \
    --set hubble.relay.enabled=true \
    --set hubble.ui.enabled=true

The --set k8sServiceHost matters because without kube-proxy, the cluster has no way to translate the in-cluster kubernetes.default.svc service to a real address — Cilium needs to know the API server's actual location for bootstrapping.

Wait for the rollout:

cilium status --wait
kubectl get pods -A

All system pods should now be Running, and a new kube-system/cilium-* DaemonSet plus cilium-operator Deployment plus hubble-relay + hubble-ui exist.

Verify the replacement

cilium connectivity test
# Runs ~80 tests across pod-to-pod, pod-to-service, pod-to-world, network policies.
# Takes 10-15 minutes; the output is verbose but each test reports pass/fail.

Hubble: the observability UI

Port-forward the Hubble UI:

cilium hubble ui

The browser opens to a real-time flow diagram of every connection in the cluster. Filter by namespace, source/destination pod, verdict (allowed/denied), or DNS query.

CLI alternative:

cilium hubble enable
hubble observe --namespace default --follow
hubble observe --pod default/web --to-port 5432       # narrow filters
hubble observe --verdict DENIED --follow              # show only blocked flows

For "what is this app actually talking to right now," Hubble is the single most useful tool you can add to a Kubernetes cluster.

Network policies

Standard networking.k8s.io/v1 NetworkPolicy works, but Cilium's cilium.io/v2 CiliumNetworkPolicy extends it to L7 (HTTP methods, paths, headers, gRPC services, DNS hostnames, Kafka topics):

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: web-to-api
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: web
  egress:
    - toEndpoints:
        - matchLabels:
            app: api
      toPorts:
        - ports: [ { port: "8080", protocol: TCP } ]
          rules:
            http:
              - method: GET
                path: "/v1/.*"
              - method: POST
                path: "/v1/events"
    - toFQDNs:
        - matchPattern: "*.googleapis.com"
      toPorts:
        - ports: [ { port: "443", protocol: TCP } ]
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system
            k8s-app: kube-dns
      toPorts:
        - ports: [ { port: "53", protocol: UDP } ]
          rules:
            dns:
              - matchPattern: "*"

This policy says: app: web can call app: api only via GET on /v1/* and POST on /v1/events; can reach any *.googleapis.com hostname over TLS; can use cluster DNS. Everything else is denied.

WireGuard between nodes

Cilium can encrypt all pod-to-pod traffic between nodes with WireGuard (kernel-native, fast):

cilium install --set encryption.enabled=true --set encryption.type=wireguard
# Or upgrade an existing install
cilium upgrade --set encryption.enabled=true --set encryption.type=wireguard

cilium status        # shows "Encryption: Wireguard"

Per-node WireGuard tunnels appear; every pod's traffic to a different node is encrypted at the kernel level. CPU cost is modest on modern processors (~5-10% per gigabit of throughput).

Cluster Mesh

For multi-cluster setups, Cilium's Cluster Mesh stitches multiple clusters into one logical network with shared service discovery and cross-cluster network policies:

cilium clustermesh enable --service-type LoadBalancer
cilium clustermesh connect --context cluster-a --destination-context cluster-b

Services in cluster-a can be exported and consumed transparently by cluster-b. Critical for multi-region disaster recovery without standing up a service mesh on top.

Tetragon: runtime security

Cilium's sister project Tetragon uses the same eBPF base for runtime security: detect process executions, file modifications, network connections, and capability use. Define policies in TracingPolicy CRDs; observe events via kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon or a SIEM forwarder (see Wazuh).

helm install tetragon cilium/tetragon -n kube-system

Performance numbers worth knowing

  • Pod-to-pod TCP latency drops ~10-30% vs Flannel+iptables for small packets.
  • Service load-balancing latency drops dramatically at high service counts — iptables service rules are O(n) per packet; eBPF maps are O(1).
  • WireGuard encryption costs ~5-10% per Gb/s on AES-NI-capable CPUs (every server-class CPU made since 2015).

When Cilium isn't the right choice

  • For very small clusters (a homelab with 2-3 nodes and a few pods), Flannel is simpler and Cilium's overhead isn't worth it.
  • For older kernel versions (<5.10), Cilium's feature surface is reduced. K3s on modern distros isn't affected.
  • For Linux-on-ARM-Cortex-A boards without recent kernel support, eBPF features may be limited — check the kernel BTF support before committing.

For any cluster where service count grows past tens, network policies become important, or you want to see what's actually flowing — Cilium is the most polished option in 2026, with Hubble as the killer add-on.