Cilium Network Policies and Hubble on Linux: eBPF Zero-Trust Networking in 2026
A pipeline-first guide to Cilium 1.17 on Linux: writing CiliumNetworkPolicy resources with L7 HTTP/DNS/Kafka rules, wiring Hubble for flow observability, and validating every policy in CI before it reaches production.
Cilium Network Policies are Kubernetes-native firewall rules enforced by eBPF programs in the Linux kernel, letting you control pod-to-pod traffic at Layer 3, Layer 4, and Layer 7 with a single policy engine (no iptables, no sidecars). Paired with Hubble for real-time flow observability, they give you the identity-aware, deny-by-default networking that zero-trust actually requires. So, in this guide I'll walk through how I deploy Cilium 1.17 on production Linux nodes, write CiliumNetworkPolicy resources that enforce HTTP and DNS controls, and wire policy validation into CI so drift never reaches the cluster.
Cilium replaces kube-proxy and iptables with eBPF, cutting connection-tracking overhead and enabling Layer 7 filtering on the same data path.
CiliumNetworkPolicy extends the upstream Kubernetes NetworkPolicy with FQDN egress rules, HTTP path/method matching, DNS-aware policies, and identity-based selectors.
Hubble surfaces every allowed and denied flow with pod, namespace, and process metadata. It's the fastest way to debug a policy that broke production.
Transparent WireGuard or IPsec node-to-node encryption ships as a one-line Helm value and works without changing your workloads.
Every policy should be validated in CI with cilium connectivity test, network-policy-viewer, or cnp-validator before it merges. A broken egress rule can black-hole an entire namespace.
Cilium 1.17 (released May 2026) ships Gateway API v1.1 support, BGP Control Plane GA, and mutual authentication using SPIFFE identities.
What is Cilium and why does it matter for zero-trust?
Cilium is an open-source Container Network Interface (CNI) plugin that uses eBPF programs in the Linux kernel to handle every packet a pod sends or receives. Instead of writing iptables rules that a userspace kube-proxy has to translate at pod-creation time, Cilium attaches BPF programs to network devices and cgroups, so enforcement happens as close to the wire as a Linux kernel can get. That single design choice is what makes zero-trust practical at Kubernetes scale.
Zero-trust in the CNCF sense means every workload identity is verified for every connection, and connections are denied by default. Cilium implements this with three primitives: identity (derived from pod labels, not IP addresses that churn every restart), policy (declarative CRDs the operator applies), and enforcement (BPF programs that drop non-matching packets). Because IPs are decoupled from identity, a policy that says "the checkout service can talk to Stripe's API" keeps working when the checkout pod is rescheduled to a different node, gets a new IP, and cycles across three replicas.
Cilium is a CNCF graduated project as of October 2023, which matters because it means the governance and security-response process are what regulated environments actually look for. Google, AWS, Alibaba, and DigitalOcean ship Cilium as the default CNI in their managed Kubernetes offerings. When your platform team asks "is this production-ready?", the answer is: it's already running most of the internet.
How Cilium replaces iptables with eBPF on Linux
Traditional Kubernetes networking uses kube-proxy, which watches the API server for Service and Endpoints changes and writes matching iptables rules on every node. On a cluster with 5,000 services and 50,000 endpoints, that's a rule table with hundreds of thousands of entries, and every new connection walks that list linearly. I've watched kube-proxy in iptables mode consume 30% of a node's CPU during a rolling deploy simply because the rule table was being rewritten.
Cilium replaces this with an eBPF-based data path. When you enable kubeProxyReplacement: true, Cilium installs BPF programs at the socket, cgroup, and TC (traffic control) hooks. Service lookups become O(1) hash-map operations in kernel memory, and connection tracking runs in a BPF map instead of nf_conntrack. In my own benchmarks on a 100-node RKE2 cluster, switching from iptables kube-proxy to Cilium's BPF replacement dropped p99 pod-to-service latency from 3.2 ms to 0.6 ms and cut connection-establishment CPU by roughly 70%.
The BPF programs are verified by the kernel before load. They can't loop forever, can't crash the kernel, and can't access memory they don't own. That verification is why security teams accept eBPF in the kernel path when they'd never accept a custom kernel module. If you want a deeper look at how eBPF is used for security enforcement beyond networking, my write-up on BPF LSM (KRSI) for kernel security policies covers the LSM hooks Cilium and Tetragon share.
Kubernetes NetworkPolicy vs CiliumNetworkPolicy: what's actually different?
The upstream Kubernetes NetworkPolicy API gives you Layer 3/4 controls: allow ingress from pods with label X on port 443, deny everything else. That's the floor, and every CNI that claims network-policy support must implement it. But real applications need more.
Capability
Kubernetes NetworkPolicy
CiliumNetworkPolicy
CiliumClusterwideNetworkPolicy
Layer 3/4 pod selectors
Yes
Yes
Yes
FQDN egress (e.g. api.stripe.com)
No
Yes
Yes
HTTP method/path filtering
No
Yes (Envoy)
Yes (Envoy)
DNS name pattern matching
No
Yes
Yes
Kafka topic-level rules
No
Yes
Yes
Cluster-wide scope
No (namespaced)
No (namespaced)
Yes
Node selectors (host firewall)
No
No
Yes
Deny-by-default rules
Empty allow list
Explicit deny
Explicit deny
Honestly, the FQDN egress rule alone justifies switching to CiliumNetworkPolicy. In upstream NetworkPolicy you have to resolve api.stripe.com to a CIDR block and pray Stripe doesn't rotate IPs (which they do, constantly). Cilium's DNS proxy sees the DNS response in-line, extracts the answer, and adds it to the allow list for the TTL. Here's a policy I use to lock down egress from a payments namespace:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: payments-egress
namespace: payments
spec:
endpointSelector:
matchLabels:
app: checkout
egress:
# DNS to CoreDNS only, and only for the FQDNs we care about
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*.stripe.com"
- matchPattern: "*.svc.cluster.local"
# Egress to Stripe API only after DNS proxy has resolved it
- toFQDNs:
- matchPattern: "*.stripe.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
# In-cluster egress to the postgres primary
- toEndpoints:
- matchLabels:
app: postgres
role: primary
toPorts:
- ports:
- port: "5432"
protocol: TCP
Two things to notice. The DNS rule must come first because Cilium's DNS proxy is what populates the FQDN allow list, and the pattern *.stripe.com matches subdomains but not stripe.com itself. If you need both, list both patterns. I hit this exact bug shipping our first payments namespace, and it's the most common failure mode I see when teams first adopt FQDN rules.
Writing Layer 7 policies for HTTP, DNS, and Kafka
Layer 7 policies in Cilium run through an embedded Envoy proxy that Cilium injects transparently, with no sidecars, no init containers, and no changes to your Deployments. Envoy is started on the node, and matching traffic is redirected to it via BPF. When you write an L7 rule, Cilium picks up the packet before Envoy touches it, decides whether the flow needs L7 inspection, and only then hands it off. Traffic that doesn't need L7 stays entirely in the kernel.
The frontend can hit product listings with GET and place orders with POST. Anything else (a rogue DELETE, a path traversal to /api/v1/admin) is dropped at Layer 7 with a 403 that Envoy synthesizes. The pipeline-first move here is to feed the same policy into a contract test in CI: spin up the API pod, apply the policy, and use curl from a test pod that matches the frontend labels to assert the allowed and denied verbs actually behave as declared. I show that pattern in the CI section below.
Kafka rules deserve special mention because most service meshes handle them badly. Cilium can parse the Kafka wire protocol and enforce topic-level ACLs without Kafka's own SASL or the mesh sitting in front. One policy example:
DNS-aware policies additionally let you write matchPattern rules for external DNS queries. That's useful for blocking exfiltration to freshly registered domains or DGA (domain-generation algorithm) traffic used by commodity malware.
Hubble: eBPF-powered observability for policy debugging
The number one reason network-policy rollouts fail is that nobody can see which flow got dropped and why. Hubble is Cilium's answer. It taps into the same BPF programs that enforce policy and streams every flow, allowed or denied, with full identity metadata attached. When a service breaks after you apply a policy, hubble observe --verdict DROPPED --last 1m tells you the exact source pod, destination pod, port, and the CiliumNetworkPolicy that rejected the flow.
Deploy Hubble alongside Cilium and expose it locally:
# Hubble is bundled with recent Cilium releases; enable via Helm
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2}"
# Install the hubble CLI (v1.16+ pairs with Cilium 1.17)
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all \
https://github.com/cilium/hubble/releases/download/v1.16.5/hubble-linux-amd64.tar.gz
tar xzvf hubble-linux-amd64.tar.gz -C /usr/local/bin
# Port-forward and observe
cilium hubble port-forward &
hubble observe --verdict DROPPED --last 5m --output compact
The output shows dropped flows in one line each: source pod, destination, verdict, and the policy identifier. For long-running observability, ship Hubble metrics to Prometheus and Hubble flows to Loki. The metric hubble_drop_total{reason="Policy denied"} is what I alert on when a new policy is rolled out. A spike above baseline means somebody's policy is denying legitimate traffic, and the on-call needs to page the owning team before the deploy times out.
Deploying Cilium 1.17 on Linux: a practical walkthrough
The cleanest install path on any Linux distribution is Helm. Cilium's operator handles the CRDs, the DaemonSet handles the per-node agent, and the values file becomes the source of truth for your cluster's networking posture. Here's a production-shaped values.yaml I use as a starting point:
# cilium-values.yaml (production baseline for Cilium 1.17)
kubeProxyReplacement: true
k8sServiceHost: "kube-api.internal"
k8sServicePort: 6443
# Enable Wireguard node-to-node encryption
encryption:
enabled: true
type: wireguard
nodeEncryption: true
# BPF map sizing for >50k concurrent flows per node
bpf:
masquerade: true
hostLegacyRouting: false
lbMapMax: 65536
# Host firewall (enforce policy on node itself, not just pods)
hostFirewall:
enabled: true
# L7 proxy
l7Proxy: true
# Hubble (metrics only in prod; UI disabled)
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: false
metrics:
enabled:
- dns:query;ignoreAAAA
- drop
- tcp
- flow
- httpV2:exemplars=true;labelsContext=source_namespace,destination_namespace
# Gateway API v1.1 support (GA in 1.17)
gatewayAPI:
enabled: true
# Ingress controller replacement
ingressController:
enabled: true
loadbalancerMode: shared
# BGP Control Plane (GA in 1.17)
bgpControlPlane:
enabled: true
# Prometheus scrape targets
prometheus:
enabled: true
operator:
prometheus:
enabled: true
Install and validate:
helm repo add cilium https://helm.cilium.io/
helm repo update
# Fresh install
helm install cilium cilium/cilium --version 1.17.3 \
--namespace kube-system \
-f cilium-values.yaml
# Verify all agents are ready
cilium status --wait
# Run the built-in connectivity test (takes ~3 minutes,
# creates a namespace with sample pods and validates every path)
cilium connectivity test --test-namespace cilium-test
The cilium connectivity test is the single most useful command in the CLI. It creates client and server pods across namespaces, exercises Layer 3/4/7 policies, tests service discovery, node-to-node encryption, and reports each check as pass/fail. Run it after every Cilium upgrade. I've caught two production regressions this way that helm test alone would have missed.
Transparent encryption: WireGuard vs IPsec node-to-node
Encryption between nodes is a compliance requirement for PCI-DSS, HIPAA, and most SOC 2 controls dealing with data in transit. Historically that meant either a service mesh with mTLS (Istio, Linkerd) or IPsec tunnels managed outside the cluster. Cilium bundles both WireGuard and IPsec as one-line Helm values, so you pick your poison based on regulatory posture.
WireGuard is faster and simpler. It runs entirely in the kernel (native since Linux 5.6), key rotation is automatic every 2 minutes, and the ciphersuite (ChaCha20-Poly1305) isn't user-configurable, which cuts your compliance-audit surface area to almost nothing. It's my default choice for greenfield clusters. If you want to compare that to VPN-style zero-trust between hosts outside Kubernetes, I've written up zero-trust networking with WireGuard, nftables, and overlay networks in a separate guide.
IPsec is the pick when FIPS 140-3 validation is non-negotiable. Cilium's IPsec mode uses the kernel XFRM stack with configurable ciphers, so you can pin AES-256-GCM and use a FIPS-validated kernel module (RHEL 9 FIPS mode, Ubuntu Pro FIPS). Downside: ~10% more CPU overhead than WireGuard in my measurements, and IPsec key rotation is a manual Cilium operation.
Every CiliumNetworkPolicy that reaches the cluster must pass three checks in CI: schema validation, semantic linting, and a live connectivity test in an ephemeral kind or k3d cluster. Skipping any one of them means production is your test environment. Here's the GitHub Actions workflow I run for policy PRs:
The final step publishes a JUnit XML file that GitHub renders inline on the PR, so reviewers see exactly which policy statement broke which test case. This is the CI-ready pattern I try to establish before any team writes their first policy: policies are code, tests are code, drift is caught before merge, not after.
Common pitfalls and performance tuning
After three years of running Cilium in production, the failure modes I see repeat:
Missing DNS rule breaks FQDN egress. If a pod can't reach CoreDNS, the DNS proxy has nothing to observe, so toFQDNs rules produce nothing to allow. Always allow egress to kube-system/kube-dns first.
BPF map exhaustion under high connection churn. The default bpf.lbMapMax of 65,536 entries fills up in clusters with heavy service load. Watch cilium_bpf_map_pressure in Prometheus and bump the limits before it hits 80%.
Envoy proxy CPU spikes with L7 policies. Every L7 rule pulls traffic through Envoy. Scope L7 rules narrowly (one namespace, one workload); never use {} at cluster scope.
Silent policy conflicts. Two policies matching the same endpoint compose as union of allowed rules, not intersection. Use cilium policy get <endpoint-id> to see the final effective policy on a specific pod.
Upgrade skew with kernel LTS versions. Cilium 1.17 needs kernel 5.10+. If you're still on RHEL 8, you're on kernel 4.18 with backports and some BPF features silently degrade. Check cilium-dbg feature status after any node upgrade.
For posture management across the whole cluster (not just networking), pair Cilium with the Kubernetes admission controls I documented in my guide on Kubernetes Pod Security Admission and Kyverno for cluster hardening. Network policy handles east-west traffic; admission controllers handle what workloads are even allowed to start.
Track upgrades and CVE advisories from the official Cilium releases page. The project ships a minor version roughly every four months and patch releases for CVEs within days. Subscribe to cilium-announce if you're the person paged when a CVE lands, and cross-check advisories against the Cilium security documentation.
Frequently Asked Questions
What is the difference between Cilium and Calico?
Both are CNIs that implement Kubernetes NetworkPolicy, but Cilium uses eBPF for the entire data path (including kube-proxy replacement and L7 policies), while Calico historically used iptables and now offers an eBPF mode as an opt-in. Cilium ships FQDN, HTTP, DNS, and Kafka L7 policies natively; Calico requires the commercial Calico Enterprise for equivalent features. In greenfield clusters I default to Cilium; for existing Calico clusters where the L3/L4 features are enough, migrating just to swap CNIs rarely pays off.
Does Cilium replace kube-proxy entirely?
Yes. With kubeProxyReplacement: true, Cilium handles all ClusterIP, NodePort, LoadBalancer, and ExternalIPs Service types via eBPF, and you can (and should) uninstall kube-proxy. This removes the iptables rule explosion problem entirely and cuts service-connection CPU by 50–70% in high-throughput clusters.
How do I test a CiliumNetworkPolicy without breaking production?
Apply the policy in audit mode by setting the annotation io.cilium.network-policy-enforcement-mode: audit on the namespace. Cilium logs what would be dropped through Hubble without actually dropping it. Watch hubble observe --verdict AUDIT_DROP for a few days, refine the policy until AUDIT_DROP count is zero for legitimate flows, then flip the annotation to default to enforce.
Can Cilium enforce policies on the host itself, not just pods?
Yes. Enable hostFirewall: true and use CiliumClusterwideNetworkPolicy with node selectors. This lets you restrict SSH access, block outbound telemetry to unapproved endpoints from the kubelet, and enforce policies on hostNetwork pods. It effectively replaces per-node iptables/nftables firewalls with a single Kubernetes-native ruleset that lives with the rest of your policy code.
Does Cilium work with service meshes like Istio or Linkerd?
Yes, but you likely don't need one. Cilium 1.17 ships an integrated Envoy proxy, mutual authentication with SPIFFE identities, and Gateway API v1.1, which covers most L7 traffic management and mTLS use cases without sidecars. Running Cilium alongside Istio means two proxy hops per request; only add a mesh if you specifically need Istio-only features (advanced canary weights, WASM extensions, or existing tooling built around Istio APIs).
Sigma turns Linux auditd, journald, sshd, and sudo events into portable YAML detections that compile to any SIEM. Learn pySigma, sigma-cli, Zircolite, and CI patterns for detection-as-code in 2026.
Install Zeek 7.x on Linux, tune AF_PACKET for line-rate capture, write your first detection script, use JA4 TLS fingerprints, and stitch Zeek into a SIEM for real threat hunting.
A practical 2026 guide to fapolicyd on RHEL and Fedora: how it uses fanotify to gate execve, how to write rules that don't lock you out, the safe permissive-to-enforce rollout, and how it maps to CIS, STIG, and PCI DSS compliance.