A reserved identity — world
cilium-dbg identity get 2 ID LABELS
2 reserved:world Identity 2 is reserved:world — 'any address outside the cluster that Cilium doesn’t manage,' i.e. the internet. No pod, no labels — just a fixed constant.
hubble observe --follow
The main comparison ends at L7 (flow observability) and L8 (network policy) — the two things eBPF can do that iptables can't. This is the deep dive behind them. Because the same in-kernel programs that route a packet also identify and judge it, you can watch enforcement happen, live, by identity. Every capture below is real, from the V2 cluster.
Cilium never reasons about pod IPs. It assigns every sender and receiver a numeric IDENTITY derived from labels, and collapses “who is this?” into a single integer the datapath checks in O(1). There are two ranges.
Reserved (1–255) — fixed, well-known, the same on every Cilium cluster. For things that aren’t normal pods: the host, the outside world, other nodes, the apiserver.
cilium-dbg identity get 2 ID LABELS
2 reserved:world Identity 2 is reserved:world — 'any address outside the cluster that Cilium doesn’t manage,' i.e. the internet. No pod, no labels — just a fixed constant.
cilium-dbg identity list # 1–255 ID LABELS
1 reserved:host
2 reserved:world
3 reserved:unmanaged
4 reserved:health
5 reserved:init
6 reserved:remote-node
7 reserved:kube-apiserver
8 reserved:ingress
9 reserved:world-ipv4
10 reserved:world-ipv6 These never change. host, world, remote-node, kube-apiserver, and so on — the vocabulary Cilium uses for non-workload traffic.
Allocated (256–65535) — minted dynamically, one per unique label set, cluster-local. These are your workloads. Same labels → same identity; delete the pod and the number is garbage-collected and reused.
cilium-dbg identity get 34708 ID LABELS
34708 k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=netflow-test
k8s:io.cilium.k8s.policy.cluster=default
k8s:io.cilium.k8s.policy.serviceaccount=default
k8s:io.kubernetes.pod.namespace=netflow-test
k8s:role=other Identity 34708 IS the client-blocked pod — computed from its labels, role=other among them. Enforcement is identity-to-identity, so it survives pods being recreated with new IPs.
Every Hubble line names a source and destination by identity, a verdict, a direction, and TCP flags. Here the two sides of the same policy: allowed vs denied.
hubble observe --to-label app=nginx --follow client-allowed:34376 (ID:63760) -> nginx:80 (ID:18565) policy-verdict:L3-Only INGRESS ALLOWED (TCP Flags: SYN)
client-allowed:34376 (ID:63760) -> nginx:80 (ID:18565) to-endpoint FORWARDED (TCP Flags: ACK) client-allowed (identity 63760) reaches nginx (18565): 'policy-verdict: L3-Only INGRESS ALLOWED', the handshake completes (SYN then FORWARDED ACK). An explicit allow, matched by identity.
hubble observe --namespace netflow-test --verdict \
DROPPED --follow client-blocked:40520 (ID:34708) <> nginx:80 (ID:18565) Policy denied DROPPED (TCP Flags: SYN)
client-blocked:40520 (ID:34708) <> nginx:80 (ID:18565) policy-verdict:none INGRESS DENIED (TCP Flags: SYN) client-blocked (identity 34708) is refused: 'Policy denied DROPPED' / 'INGRESS DENIED' — and it happens at the very first SYN, so the handshake never begins. Same destination, different source identity, opposite outcome.
The only difference between these two is the role label → a different identity → an allow entry, or not. No IPs anywhere in the decision.
nginx has a public LoadBalancer IP, so the open internet finds it. This flow was generated by curling that IP from outside the cluster — it shows up as reserved:world and the ingress policy refuses it, at the eBPF layer, before it reaches the pod.
curl http://34.72.78.182 # from outside the cluster 162.195.118.115:63031 (world) <> nginx:80 (ID:18565) Policy denied DROPPED (TCP Flags: SYN)
162.195.118.115:63031 (world) <> nginx:80 (ID:18565) policy-verdict:none INGRESS DENIED (TCP Flags: SYN) A real external IP (162.195.118.115) → nginx, collapsed to identity 2 (world) and dropped. Your public Service is effectively private to role=frontend — many source IPs, one identity, one policy decision.
A second policy governs the other direction: an egress rule that lets the egress-demo pod reach only nginx. Everything else outbound is dropped on the way out — and world shows up again, this time as the denied destination.
hubble observe --from-pod netflow-test/egress-demo \
--verdict DROPPED --follow egress-demo:51920 (ID:58472) <> 1.1.1.1:80 (world) Policy denied DROPPED (TCP Flags: SYN) egress-demo (identity 58472) → 1.1.1.1, which resolves to identity 2 (world): 'Policy denied DROPPED', enforced at the pod’s own node before the packet leaves. Same identity, same mechanism — direction flipped.
Ingress guards who gets in; egress guards where you can go.
Same identity model, same eBPF allowlist, opposite enforcement point —
and world (identity 2) sits on both ends of the story.