This episode guides you through creating your first CiliumNetworkPolicy: the CNP structure, target selection with label selectors, and ingress and egress rules with ports and ranges. You will also apply the policy and test it with hubble observe to make sure the allow-list works as expected.

We have mastered the identity concept in episode 5. Now it is time to translate the concept into a real policy. Episode 6 introduces CiliumNetworkPolicy (CNP) — the Kubernetes CustomResource that is the main tool for applying network security in Cilium.
We will dissect the CNP structure, write ingress and egress rules based on label selectors and ports, then apply and test them with hubble observe. After this episode, you will be able to write allow-list policies for simple applications — the foundational skill used in almost all subsequent security episodes.
CNP is a Kubernetes resource scoped to a single namespace. Its core part consists of three blocks: endpointSelector to select the protected pods, then ingress and egress to define the traffic allowed in and out. The basic structure to remember:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-rule-contoh
spec:
endpointSelector:
matchLabels:
app: frontend
ingress:
- fromEndpoints:
- matchLabels:
app: backend
toPorts:
- ports:
- port: "8080"
protocol: TCPapiVersion: cilium.io/v2 marks this resource as a CNP. spec.endpointSelector selects the pods protected by the policy; fromEndpoints inside ingress determines which identities are allowed to reach it.
When a CNP with an ingress block is applied, the selected endpoints switch to default deny for ingress. All incoming traffic that does not match a rule is dropped. Let us write a policy that only allows pods with the label app=backend to reach port 8080 on pods with app=frontend:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-ingress
spec:
endpointSelector:
matchLabels:
app: frontend
ingress:
- fromEndpoints:
- matchLabels:
app: backend
toPorts:
- ports:
- port: "8080"
protocol: TCPNote that fromEndpoints uses a label selector, not IPs. New backend pods with the same labels are automatically allowed, and changes to the frontend pod's IP do not affect the rule at all. To apply the policy:
kubectl apply -f allow-ingress.yaml
kubectl get cnpkubectl apply -f allow-ingress.yaml applies the policy to the cluster, and kubectl get cnp shows the list of CNPs along with the number of active rules.
Egress governs traffic leaving the endpoint. Egress rules usually target other pods, the DNS service, or the internet with port restrictions. An example policy allowing app=frontend pods to reach a database in another namespace:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-egress-db
spec:
endpointSelector:
matchLabels:
app: frontend
egress:
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: database
matchLabels:
app: postgres
toPorts:
- ports:
- port: "5432"
protocol: TCPmatchLabels: app: postgres selects the database pods, and the combination with io.kubernetes.pod.namespace: database restricts the scope to a specific namespace. As long as no other broader egress policy exists, once this policy is applied, frontend pods may only leave to port 5432 on the database.
Once the policy is in place, it is time to verify. Enable Hubble first if not already done (episode 7), then observe the flows:
hubble observe --namespace default
hubble observe --verdict DROPPEDhubble observe --namespace default shows the flows in the default namespace, including FORWARDED verdicts for allowed traffic. hubble observe --verdict DROPPED shows traffic rejected by policy — the fastest way to confirm the allow-list works: traffic other than what is defined must appear as DROPPED.
For a quick simulation, create two pods with different labels then test the connection:
kubectl run frontend --image=nginx --labels=app=frontend
kubectl run backend --image=curlimages/curl --command -- sh -c "sleep 3600"
kubectl exec backend -- curl -s -o /dev/null -w "%{http_code}" http://frontendkubectl exec backend -- curl -s http://frontend will succeed (code 200) because backend is allowed by the policy. Try running it from a pod with another label, for example app=worker, and that request must fail or time out — that is the proof that default deny is working.
Tip
Do not write a policy in one big application. Start with a small rule, apply it, observe with Hubble, then expand. This pattern minimizes the risk of blocking traffic that should be allowed — a habit that will be useful in episode 18 when policies are managed through GitOps.
Key takeaways:
kubectl apply and kubectl get cnp for managing policies.hubble observe --verdict DROPPED to prove the allow-list is working.In the next episode 7, we will explore Hubble thoroughly — enable the relay and UI, read flow logs with hubble observe, trace allowed connections, verify policies, and find dropped traffic. This observability is what lets every policy experiment in this episode be analyzed with evidence, not guesswork.