Cilium’s endpoint policies are how you enforce granular network security down to the individual pod level, and they operate on a fundamentally different principle than traditional network segmentation.

Let’s see it in action. Imagine we have two identical deployments, app-a and app-b, both running a simple web server.

# app-a.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-a
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-a
  template:
    metadata:
      labels:
        app: app-a
    spec:
      containers:
      - name: web
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80
# app-b.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-b
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-b
  template:
    metadata:
      labels:
        app: app-b
    spec:
      containers:
      - name: web
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80

By default, with no policies, pods from app-a can talk to pods from app-b and vice-versa.

Now, let’s create a policy that denies all traffic between app-a and app-b.

# deny-a-to-b.yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: deny-app-a-to-app-b
spec:
  endpointSelector:
    matchLabels:
      app: app-a
  deny:
  - toEndpoints:
    - matchLabels:
        app: app-b

Apply this policy: kubectl apply -f deny-a-to-b.yaml.

If we try to curl from a pod in app-a to a pod in app-b (e.g., kubectl exec -it <app-a-pod-name> -- curl http://<app-b-pod-ip>), the connection will time out. This is because the policy explicitly denies this flow.

The core problem Cilium Network Policies solve is moving security from network perimeter concepts (like firewalls between subnets) to identity-based controls at the workload level. Instead of saying "block traffic from CIDR X to CIDR Y," you say "block traffic from pods labeled 'app: app-a' to pods labeled 'app: app-b'." This identity is often derived from Kubernetes labels, but can also be based on service accounts, Kubernetes namespaces, or even DNS names.

Cilium leverages eBPF to enforce these policies directly in the kernel, right where network packets are processed. When a packet leaves a pod, the kernel checks if there’s a policy associated with the source endpoint. If there is, it evaluates the policy rules. If the destination endpoint matches a deny rule, the packet is dropped. If it matches an allow rule, it’s forwarded. If no rule matches, the default policy (usually "allow all" if no policies are defined, or "deny all" if any policy is defined and no explicit allow exists) takes effect.

To illustrate, let’s create a policy that allows app-a to talk to app-b on port 80, but only if the source pod has the label access: frontend.

First, let’s label one of the app-a pods: kubectl label pod <app-a-pod-name> access=frontend.

Now, the policy:

# allow-frontend-to-b.yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-app-b
spec:
  endpointSelector:
    matchLabels:
      app: app-a
      access: frontend # Only pods with this label will be subject to this rule
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: app-b
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP

Apply this: kubectl apply -f allow-frontend-to-b.yaml.

Now, if you curl from the pod labeled access: frontend in app-a to a pod in app-b, it will succeed. If you try from another pod in app-a without that label, it will fail (assuming the deny-app-a-to-app-b policy is still active, or if no other allow rule permits it).

The power comes from combining endpointSelector, ingress, egress, toEndpoints, fromEndpoints, toPorts, and fromPorts. You can build incredibly specific rules. For instance, you could allow pods in namespace prod with label role: backend to only talk to pods in namespace prod with label role: database on port 5432 via TCP.

A common pitfall is confusing endpointSelector with toEndpoints or fromEndpoints. The endpointSelector defines which pods the policy applies to (the source of the traffic being evaluated), while toEndpoints and fromEndpoints define the destination or source of the traffic that the policy rules apply to. If you want to restrict traffic from app-a to app-b, your endpointSelector will target app-a, and your toEndpoints will target app-b.

When you apply any CiliumNetworkPolicy, Cilium automatically installs a default "deny all" policy for any endpoint that has any policy applied to it. This means if you have even one allow policy, and a pod doesn’t match any allow rules, it will be denied network access. You must explicitly allow what you want, or ensure your endpointSelector is broad enough to catch all relevant pods if you want to rely on a default allow for non-matching traffic.

The next step in mastering Cilium policies is understanding how to use toCIDR and fromCIDR for external traffic and how to leverage service.name and service.namespace selectors for Kubernetes Service-aware policies.

Want structured learning?

Take the full Cilium course →