This post teaches the Cilium policy model with clear scenarios and annotated YAML. It matches the style of practical technical blogs, explanation first and code second, with links to the official docs where you will want deeper detail.
Why Cilium policy
Kubernetes’ built-in NetworkPolicy objects define which pods can communicate using label-based rules at the IP and port level. This provides basic isolation, but it stops short of deeper visibility or intent-based control.
Cilium builds on this foundation by introducing security identities derived from labels. These identities represent workloads consistently across nodes and are enforced directly in the kernel using eBPF. Because enforcement happens in the datapath, policies remain accurate and efficient even as workloads scale or IPs change.
Beyond IP and port filtering, Cilium understands application context such as DNS names and HTTP methods and paths. This makes it possible to express policies in human terms — for example, “allow only GET requests on /health from pods with role=frontend” or “allow egress only to api.partner.com.”
Together, these capabilities create a single, consistent model for enforcing and observing network behavior across all workloads. This post walks through that model step by step, with practical YAML examples you can apply to your own environment.
For further reading, see the official Cilium policy overview for the complete language reference and selector options.
Mental model
Every policy answers four things. Where to enforce, which direction to guard, who may talk, and whether to apply checks at the application layer.
These are the top things to keep in mind when defining a Cilium Network Policy.
- Subject, choose pods with
endpointSelectoror nodes withnodeSelector - Direction, if a selected subject has an
ingresslist then ingress becomes default deny for that subject, the same idea applies foregress - Peers, choose with
fromEndpoints,toEndpoints,toEntities,toCIDRSet,toFQDNs,toServices - Application layer, add optional
rules:undertoPortsfor HTTP or DNS
Language details are in the Cilium policy language.
We typically refer to the security policies implemented in Cilium holistically as “Cilium Network Policy”. However when you dive into using them in your platform, you will find there is in fact two types of policy configuration to be aware of. Essentially most of the information in this post is true for both types. But just keep in mind the following;
- CiliumNetworkPolicy (CNP) is the namespaced policy object you apply to control traffic for pods within a single namespace.
CiliumClusterwideNetworkPolicy (CCNP) is the cluster-scoped version. It uses the same language and selectors but applies across all namespaces, which is useful for node policies, global DNS interception, or rules that span multiple teams.
What a Cilium endpoint is
Every pod (and any process that Cilium manages traffic for) is represented inside Cilium as an endpoint. An endpoint is essentially Cilium’s view of a workload: its labels, Security Identity, policies, and network state.
When you write a policy with an endpointSelector, you’re telling Cilium “apply this rule to the endpoints whose labels match this selector.” Cilium uses that to program the eBPF datapath on the node where each endpoint lives.
You can see endpoints on a node with:
kubectl -n kube-system exec -ti ds/cilium -- cilium endpoint list
-
ENDPOINT: This is Cilium’s internal endpoint ID on that node. In this case,
24. You’ll use this ID if you runcilium endpoint get 24for detailed info. -
POLICY (ingress / egress): Shows whether policy enforcement is active on this endpoint. “Disabled” means there are no Cilium policies selecting it yet for that direction, so all traffic is allowed. Once you create a CiliumNetworkPolicy with an
ingressoregresssection matching this endpoint, this field will flip to “Enabled”. -
IDENTITY: The numeric Security Identity assigned to the set of identity-relevant labels for this endpoint (
69014here). Cilium uses this number in the datapath to represent the workload. -
LABELS (source:key=value): The full list of labels that Cilium knows for this endpoint. The prefix shows where the label came from (
k8s:means Kubernetes label). These are the labels you match on in your policies. In the example, it includesapp=minio, the namespace, the service account, and some Helm-related labels. -
IPv4 / IPv6: The IP addresses currently assigned to that pod. Notice you never use them directly in your policies; Cilium maps them to the Security Identity automatically. Note: there is the ability to specify CIDR-based filtering in a Cilium Network Policy as well, but this is recommended not to be used to for filtering when it comes to Pod traffic inside the cluster.
-
STATUS: Shows the endpoint’s state from Cilium’s perspective (“ready” means it’s healthy and being managed).
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS
ENFORCEMENT ENFORCEMENT
24 Disabled Disabled 69014 k8s:app.kubernetes.io/managed-by=Helm 10.0.0.247 ready
k8s:app=minio
k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=minio
k8s:io.cilium.k8s.policy.cluster=kind
k8s:io.cilium.k8s.policy.serviceaccount=quickstart-sa
k8s:io.kubernetes.pod.namespace=minio
k8s:v1.min.io/console=quickstart-console
k8s:v1.min.io/pool=ss-0
This view is invaluable when troubleshooting, and we’ll cover this towards the end of the blog post.
Labels and Security Identity
Cilium doesn’t match pods by IP addresses. Instead, it builds a Security Identity from a workload’s Kubernetes labels. Cilium takes the subset of labels that are relevant for identity, hashes them, and assigns a numeric ID. This ID is what the datapath enforces at runtime, while you continue to write policies in human-readable labels.
This means that when the same workload appears on a different node, or a pod is rescheduled with a new IP, it keeps the same Security Identity and your rules still apply without change. It also allows Cilium to cache and distribute identities efficiently across the cluster.
Practical tips for labels and identity
-
Decide on a small, consistent set of security-relevant labels (for example
app,role,team,environment) and use them across your namespaces. These become your “vocabulary” for writing policies. -
Avoid putting high-cardinality or rapidly changing labels into the identity set, because that explodes the number of unique IDs and can make policy management harder.
-
Document which labels matter for security so that developers and platform teams add them consistently. This is the key to writing clear, reusable Cilium policies that scale with your cluster.
Below is a clean simple example showing the use of labels, the other components and language used in the policy will be explained as we continue through this blog post.
-
The Deployment gives every replica the same
appandrolelabels. -
Cilium builds a single Security Identity from that label set and enforces the policy for all replicas, even as IPs change.
-
The CNP shows how you write policies once in terms of labels rather than IPs, and Cilium does the rest.
# A single application deployment with clear, security-relevant labels
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
namespace: payments
spec:
replicas: 3
selector:
matchLabels:
app: api
role: backend
template:
metadata:
labels:
app: api
role: backend # this label is part of the Security Identity
spec:
containers:
- name: api
image: my-api:latest
---
# A CiliumNetworkPolicy that selects pods by those same labels
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-web-to-api
namespace: payments
spec:
endpointSelector:
matchLabels:
app: api
role: backend # Cilium turns these labels into a Security Identity
ingress:
- fromEndpoints:
- matchLabels:
app: web # only pods with app=web can reach the API
toPorts:
- ports:
- port: "8080"
protocol: TCP
Entities at a glance
Entities are Cilium’s built-in selectors. They save you from having to hard-code addresses or write long selectors for common groups. You can think of them as shortcuts for big sets of endpoints.
| Entity | What it selects | Use case |
|---|---|---|
cluster |
Pods and nodes inside the cluster | Allow broad east–west communication as a baseline |
world |
Anything outside the cluster | Coarse egress control. |
kube-apiserver |
The control plane endpoint | Ensure workloads can talk to the API server if needed |
host |
The local node | Control node-local daemons or host networking pods |
remote-node |
Any other node | Special cases like node-to-node probes |
ingress, health |
Cilium ingress, health checks | Permit Cilium’s own internal components |
How to use these entities correctly?
- Start with cluster to allow internal traffic when you are just adopting Cilium policies, then replace it with more precise selectors as you gain confidence.
- Combine entities with fromRequires or toPorts to narrow down the allowed paths.
- Be careful with remote-node. In some environments, NAT can make outside traffic appear as if it came from a node, which could open a hole you didn’t intend.
- Use world as a guardrail. Pair it with FQDN or CIDR rules so that you explicitly control which destinations on the internet are reachable.
Policy model and default deny
Cilium enforces policy per direction.
This is different from Kubernetes NetworkPolicy, which always starts from “allow nothing” for pods that match a policy.
With Cilium:
-
As soon as a pod is selected by a Cilium policy that contains an
ingresssection, ingress moves to default deny for that pod. Only traffic explicitly allowed by the listed rules is accepted. -
The same behaviour applies for
egress. Any egress section activates default deny on egress for that pod. -
Until a direction is covered by any rule, that direction is effectively “open” for that pod.
This lets you phase in enforcement gradually: start with audit or specific traffic, then move to full default deny once you have the allow list defined.
You can control this behaviour with the enableDefaultDeny switches at the top level of a policy object. This is especially useful when you want to observe certain traffic (like DNS) without enforcing default deny yet.
In this snippet:
-
enableDefaultDeny.ingress: falseandenableDefaultDeny.egress: falsetell Cilium not to flip the endpoints into default deny posture when this policy is applied. -
The rule only intercepts DNS to
kube-dnsfor observation. -
All other traffic continues as before, until you add explicit allow/deny lists.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: dns-observe-no-deny
namespace: payments
spec:
description: Intercept DNS without forcing default deny yet
enableDefaultDeny: # override posture per direction
ingress: false
egress: false
endpointSelector: {} # apply to all endpoints in this namespace
egress:
- toEndpoints:
- matchLabels:
k8s-app: kube-dns
io.kubernetes.pod.namespace: kube-system
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*"
I produced a video on this feature when it was released in Isovalent’s enterprise Cilium edition, this feature is now also available in Cilium OSS.
One policy, explained line by line
Scenario: You run a payments API inside Kubernetes. It should only be reachable from your web front-end pods, and even then only on a small set of HTTP paths. On the other side, the API itself needs to make outbound calls , it must resolve DNS, talk to a SaaS service over HTTPS, and reach a partner network over a defined IP range.
This example shows how to express all of those requirements in a single CiliumNetworkPolicy using label selectors, entities, HTTP rules, FQDN rules, and a CIDR with an exception.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: payments-api-policy
namespace: payments
spec:
endpointSelector: # (1)
matchLabels:
app: api
ingress: # (2)
- fromEndpoints:
- matchLabels:
app: web # (3)
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http: # (4)
- method: GET
path: ^/public/.*$
- method: POST
path: ^/orders$
egress: # (5)
- toEntities: [ cluster ] # (6)
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns # (7)
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: api.example.com # (8)
toPorts:
- ports:
- port: "443"
protocol: TCP
- toCIDRSet:
- cidr: 203.0.113.0/24 # (9)
except:
- 203.0.113.128/25
- (1) Subject selector, pods with app equals api
- (2) Any ingress rule activates default deny on ingress for the subject
- (3) Only pods with app equals web may connect
- (4) HTTP rules restrict the surface to two paths
- Regex is only supported for certain Layer 7 rules in Cilium Network Policies.
- The regex used in this example, in policy terms, would let you allow or deny requests to any path under
/public/(for example/public/contact-us.html). The second regex only allows the exact path/orders, therefore/orders/listwould not be allowed.
- (5) Any egress rule activates default deny on egress for the subject
- (6) Allow in cluster peers as a baseline
- (7) Allow DNS to kube dns so FQDN rules can work and be observed
- (8) Allow HTTPS only to api.example.com
- (9) Allow a partner range with an exception
Key takeaways
-
Subject:
endpointSelectormatches pods withapp=apiin thepaymentsnamespace. That’s the group of pods Cilium will enforce this policy on. -
Ingress: only pods with
app=webcan connect on port 8080. HTTP rules then narrow the surface further to just two paths. -
Egress: permit traffic inside the cluster, allow DNS to kube-dns so name-based rules work, allow a single SaaS hostname on 443, and allow a partner CIDR with an exception.
-
Default-deny posture: the moment you add an
ingressoregresslist, that direction flips to default deny for the selected pods. All other traffic in that direction is dropped unless you explicitly allow it.
For full selector syntax and layer-seven rules see the Cilium policy language docs. For name-based egress behaviour see the DNS-based policy guide.
- FQDN policy depends on DNS interception. Always allow DNS before writing name-based rules so lookups succeed and the rules match as expected.
Common patterns you will reuse
Once you understand selectors and default-deny, most policies boil down to a few recurring shapes. These examples show the YAML you’ll write again and again, with a note on when to reach for each one.
Allow inside the namespace as a safe starting point
Use this when you’re just starting policy work and don’t want to break intra-namespace communication. It keeps pods talking to each other but lets you begin applying rules gradually.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-within-namespace
namespace: shop
spec:
endpointSelector: {} # select all pods in this namespace
ingress:
- fromEndpoints: [ { } ] # allow traffic from all pods in this namespace
egress:
- toEndpoints: [ { } ] # allow egress to all pods in this namespace
Service-to-service by labels
Use this to explicitly allow one labelled workload to reach another on a defined port.
spec:
endpointSelector:
matchLabels: { app: api } # subject pods
ingress:
- fromEndpoints:
- matchLabels: { app: web } # allowed source pods
toPorts:
- ports: [ { port: "8080", protocol: TCP } ]
Only in-cluster egress
Use this to block internet access but leave east–west traffic open.
spec:
endpointSelector:
matchLabels: { app: payments }
egress:
- toEntities: [ cluster ] # only allow traffic inside the cluster
Cross-namespace selection with requires
Use this to permit a subset of pods from another namespace by combining pod and namespace labels. This is the safest way to select across namespaces.
ingress:
- fromEndpoints:
- matchLabels: { role: batch } # pods with this role
fromRequires:
- matchLabels:
kubernetes.io/metadata.name: jobs # and only from this namespace
Pair a pod label with a namespace label to keep matches precise. See the language reference for the selector rules.
Target a Service and follow its backends
Use this to allow traffic to a Kubernetes Service by name. Cilium tracks the backends automatically as they scale.
spec:
endpointSelector:
matchLabels: { app: reporter }
egress:
- toServices:
- k8sService:
namespace: observability
serviceName: clickhouse
Service based policy is described in the language reference.
CIDR set with an exception
Use this when you must allow an IP range but exclude a sensitive block inside it.
egress:
- toCIDRSet:
- cidr: 203.0.113.0/24
except:
- 203.0.113.128/25
Application layer rules: HTTP and DNS
Cilium lets you go beyond port-based rules. You can apply optional Layer 7 filters for HTTP and DNS so that your policies express what traffic is allowed, not just where it goes.
- HTTP rules let you limit methods and paths on a given port. For example, allow only GET
/healthor POST/ordersfrom specific clients. - DNS rules let you intercept and allow or deny DNS queries, which enables FQDN-based egress policies.
Use L7 rules sparingly and deliberately. They run through an Envoy proxy built into Cilium, so they give you deep control and visibility but add a small amount of extra latency and complexity. Keep most traffic at Layer 4, use Layer 7 where you truly need method/path or name-based control.
Example: HTTP allow list on one port
Allow only a specific set of HTTP methods and paths to your service.
toPorts:
- ports: [ { port: "8080", protocol: TCP } ]
rules:
http:
- method: GET
path: ^/public/.*$
- method: POST
path: ^/orders$
This restricts incoming calls to GET /public/* and POST /orders on port 8080. All other methods or paths on that port will be denied (verdict L7 deny in Hubble).
Example: DNS interception for FQDN rules
Allow pods to resolve DNS queries to kube-dns so FQDN rules can work.
egress:
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports: [ { port: "53", protocol: UDP } ]
rules:
dns: [ { matchPattern: "*" } ]
This intercepts and allows DNS queries to kube-dns. You must have this before any toFQDNs rules will match because Cilium needs to see the DNS answer to build its IP-to-name map.
Tips for L7 rules
- Use L7 filtering on ports where you know the protocol. If the service does not speak HTTP on that port, the proxy cannot parse it.
- Always allow DNS egress first before writing FQDN rules.
- Watch Hubble verdicts
L7 denyto see if your regex or methods need adjustment. - Keep regex patterns simple and anchored. Test them in a staging cluster first.
Deny policies
Deny policies let you block specific peers or ports at Layer 3 and Layer 4. Deny takes precedence over allows. Use this to carve out sensitive paths while you keep simple allow lists elsewhere.
- Match subjects with
endpointSelectorornodeSelector. - Block by peers with
fromEndpoints,toEndpoints,toEntities,toCIDR, ortoCIDRSet. - Limit the deny to certain ports with
toPorts.
Note, deny policies match at Layer 3/4. They cannot target specific URLs or domain names. Use an allow list for names instead. See the deny policy reference.
Example, deny ingress from a specific namespace
Scenario, protect the coreapi service from batch jobs that run in the jobs namespace.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: coreapi-deny-from-jobs
namespace: tenant-jobs
spec:
endpointSelector:
matchLabels:
app: coreapi
ingressDeny:
- fromEndpoints:
- matchLabels:
kubernetes.io/metadata.name: jobs
Test, from any pod in the jobs namespace, try to call coreapi:9080. You should see Hubble output like this:
❯ hubble observe -n tenant-jobs --verdict DROPPED -f Oct 6 14:13:51.938: tenant-jobs/netpod:54534 (ID:77665) <> tenant-jobs/coreapi-95dcdb48f-j26sf:9080 (ID:72923) policy-verdict:none EGRESS DENIED (TCP Flags: SYN) Oct 6 14:13:51.938: tenant-jobs/netpod:54534 (ID:77665) <> tenant-jobs/coreapi-95dcdb48f-j26sf:9080 (ID:72923) Policy denied DROPPED (TCP Flags: SYN)
This confirms the deny policy is in effect and that the flow was dropped.
Example, deny egress to a partner CIDR
Scenario, prevent the crawler app from reaching an external range.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: crawler-deny-partner-cidr
namespace: tenant-jobs
spec:
endpointSelector:
matchLabels:
app: crawler
egressDeny:
- toCIDRSet:
- cidr: 203.0.113.0/24
Test, from a crawler pod attempt curl http://203.0.113.5. Watch with hubble observe --namespace tenant-jobs --verdict DROPPED.
Example, deny only HTTP to the internet, allow HTTPS via normal policy
Scenario, you want TLS only. Deny port 80 to the outside world, keep your existing allows for port 443 intact.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: app-deny-world-http
namespace: tenant-jobs
spec:
endpointSelector:
matchLabels:
app: web
egressDeny:
- toEntities:
- world
toPorts:
- ports:
- port: "80"
protocol: TCP
Test, from a web pod do curl http://example.org. That should be denied. curl https://example.org will follow your existing allows.
Tips
- Deny is evaluated before allows. If a flow matches a deny entry it will be dropped.
- Keep deny rules small and explicit. Use them to block what should never happen, and express the rest as allows.
- For domain based behavior use allow lists with DNS interception, not deny. Deny cannot match names.
Host firewall and node policies
Cilium policies don’t have to stop at pods. You can also control traffic to and from the host network stack; for example, SSH on the node, kubelet ports, or any host-level daemon. This is called the host firewall.
Host policies use the same language as CiliumNetworkPolicy but with a cluster-wide scope and a nodeSelector instead of an endpointSelector. Because nodes are critical, start with a single node and test carefully to avoid locking yourself out.
Example: allow only cluster-internal traffic to nodes
Use this to restrict node-level access so that only other cluster components can reach host services.
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: node-access-controls
spec:
nodeSelector:
matchLabels:
kubernetes.io/os: linux # apply to all Linux nodes
ingress:
- fromEntities: [ cluster ] # allow only cluster-internal sources
There’s a fantastic blog post on this feature in more detail on the cilium.io blog by Isovalent’s Paul Arah.
Tips for host policies
- Always start in a staging or single-node environment before applying widely.
- Combine
nodeSelectorwith specific labels (for example a node pool name) to roll out gradually. - Keep a maintenance window open with a privileged channel (for example console or bastion) in case you need to revert.
- Review the host firewall guide for supported features and caveats.
Observe and troubleshoot with Hubble and the Cilium CLI
When a policy behaves differently from what you expect, look at two views in parallel. Hubble shows what is happening on the wire with policy verdicts. The Cilium CLI shows what the agent believes about an endpoint, its labels, identity, and enforcement state. Together these let you confirm intent, locate missing rules, and fix selector mistakes quickly.
For installation and usage, see the official Hubble guide and creating policies from verdicts.
Final content promo is a video I recorded a while ago on using the Hubble CLI! To go along with the Hubble CLI Cheatsheet I created.
Step one, watch flows and verdicts with Hubble
# follow recent flows with policy verdicts from this namespace hubble observe --namespace tenant-jobs --last 50 -t policy-verdict # show only drops hubble observe --namespace tenant-jobs --verdict DROPPED # output as JSON for jq exploration or use in another system hubble observe --namespace tenant-jobs --since 5m -o json
By default, Hubble will give you the output in the “compact” view, which is nice and human-readable easily. For more detail you’ll want to use the
--output/-oargument and select a suitable option such as
JSON for more detailed information.
Example, DNS from crawler to CoreDNS, forwarded and parsed at L7
hubble observe --from-pod tenant-jobs/crawler --to-pod kube-system/coredns --protocol DNS -o JSON
... "pod_name":"crawler-575b694bcd-hcpqn", "verdict":"FORWARDED",
"l7":{"type":"REQUEST","dns":{"query":"api.github.com.","qtypes":["A"]}},
"traffic_direction":"EGRESS",
"Summary":"DNS Query api.github.com. A" ...
This shows the request from crawler to CoreDNS, parsed as DNS at L7, and allowed.
Example, egress to api.github.com allowed by a specific policy
hubble observe --from-pod tenant-jobs/crawler --protocol HTTP -o json
... "pod_name":"crawler-575b694bcd-hcpqn", "verdict":"FORWARDED",
"destination_names":["api.github.com"],
"traffic_direction":"EGRESS",
"egress_allowed_by":[{"name":"crawler","namespace":"tenant-jobs", ...}],
"Summary":"TCP Flags: SYN" ...
Here Hubble tells you which object allowed the flow. The field egress_allowed_by points to the crawler policy in the tenant-jobs namespace, which allows egress to the internet on ports 80 and 443.
How to read verdicts and fields
Hubble prints two kinds of information depending on how you run it:
- Policy verdict view (
-t policy-verdict) shows the high-level decision:ALLOWED,DENIED, orAUDITED. - Full JSON view (
-o json) shows the low-levelverdictfield (FORWARDED,DROPPED,AUDIT, etc.) plus extra fields that tell you why.
Policy-verdict output
| Verdict | Meaning | Next step |
|---|---|---|
ALLOWED |
Traffic matched an allow rule. | Nothing to do; confirm the policy name in ingress_allowed_by or egress_allowed_by if you want to see which rule matched. |
DENIED |
Traffic was blocked either because no allow matched under default deny or because an explicit deny rule matched. | Check label selectors, direction, and ingress_denied_by or egress_denied_by fields in JSON to see which policy caused it. |
AUDITED |
Traffic would have been denied but audit mode allowed it. Used to discover needed allows without breaking apps. | Review the policy, then move from audit to enforce once you are ready. |
Full JSON verdict field
When you run hubble observe -o json, the verdict field inside each flow object shows what the datapath did to that packet or request. Common values:
FORWARDED– the packet/request was allowed and forwarded.DROPPED– the datapath dropped it.drop_reason_descexplains why, for example Policy denied.AUDIT– the packet would have been dropped but was allowed due to audit posture.REDIRECTED– the flow was sent to a proxy (HTTP, DNS, Kafka).TRACED/TRANSLATED– informational events about tracepoints or address translation.
Tip: JSON output also includes ingress_allowed_by, egress_allowed_by, ingress_denied_by, and egress_denied_by fields. These list the exact Cilium policy names or rules that matched the flow. For L7 policies this information may be missing because the decision happens inside Envoy, but you will still see the high-level verdict and the HTTP or DNS record.
See the official Policy creation from verdicts guide and the Flow protocol reference for all fields.
Step two, inspect the workload as a Cilium endpoint
After watching flows in Hubble, the next step is to look at what Cilium knows about a specific workload. Every pod attached to Cilium is represented as a Cilium endpoint. This view shows the identity, labels, and realized policy state that Cilium is enforcing for that workload.
# list endpoints on this node kubectl -n kube-system exec -ti ds/cilium -- cilium endpoint list
On this demo cluster, the endpoint list output includes the coreapi pod:
ENDPOINT IDENTITY STATE ENCAP IPV4 ENCAP IPV6 NAMESPACE POD NAME 3895 72923 ready 10.0.0.244 tenant-jobs coreapi-95dcdb48f-j26sf
This shows the endpoint ID 3895, its security identity 72923, IP address, namespace, and pod name. Use the endpoint ID to get a full view:
kubectl -n kube-system exec -ti ds/cilium -- cilium endpoint get 3895
That expands into JSON. Key fields from the output on this cluster:
"external-identifiers": {
"k8s-namespace": "tenant-jobs",
"k8s-pod-name": "coreapi-95dcdb48f-j26sf"
},
"identity": {
"id": 72923,
"labels": [
"k8s:app=coreapi",
"k8s:io.kubernetes.pod.namespace=tenant-jobs"
]
},
"policy": {
"policy-enabled": "both",
"allowed-egress-identities": [16777218],
"allowed-ingress-identities": [1]
},
"proxy-statistics": [
{
"location": "ingress",
"port": 9080,
"protocol": "http",
"statistics": {
"requests": {"forwarded": 1514, "received": 1514},
"responses": {"forwarded": 1514, "received": 1514}
}
},
{
"location": "egress",
"port": 53,
"protocol": "dns",
"statistics": {
"requests": {"forwarded": 11992, "received": 11992},
"responses": {"forwarded": 11992, "received": 11992}
}
}
]
This tells us:
- The endpoint belongs to
tenant-jobs/coreapiwith identity72923. - Ingress policy is enabled on port 9080 HTTP, with rules allowing traffic from
resumes,recruiterandjobpostingpods. - Egress policy is enabled to kube-dns on port 53, with DNS proxy counters incrementing as name lookups occur.
Because we can see the live proxy counters and which rules each L4/L7 selector came from, we know exactly which policies are active, which ports and peers are permitted, and that DNS interception is working for FQDN rules.
You can also run cilium policy get to view all currently loaded policies in the agent.
In my cluster the coreapi endpoint shows an identity of 72923, ingress HTTP rules allowing calls from the resumes, recruiter and jobposting pods on port 9080, and egress rules allowing calls to elasticsearch-master on port 9200. DNS proxy statistics increment as it resolves hostnames because the allow-dns policy is applied. Below is a concatenated ouput.
{
"endpointSelector": {
"matchLabels": {
"any:app": "coreapi",
"k8s:io.kubernetes.pod.namespace": "tenant-jobs"
}
},
"ingress": [
{
"fromEndpoints": [
{ "matchLabels": { "k8s:app": "resumes", "k8s:io.kubernetes.pod.namespace": "tenant-jobs" } },
{ "matchLabels": { "k8s:app": "recruiter", "k8s:io.kubernetes.pod.namespace": "tenant-jobs" } },
{ "matchLabels": { "k8s:app": "jobposting", "k8s:io.kubernetes.pod.namespace": "tenant-jobs" } }
],
"toPorts": [
{
"ports": [ { "port": "9080", "protocol": "TCP" } ],
"rules": { "http": [ {} ] }
}
]
}
],
"egress": [
{
"toEndpoints": [
{ "matchLabels": { "k8s:app": "elasticsearch-master", "k8s:io.kubernetes.pod.namespace": "tenant-jobs" } }
],
"toPorts": [ { "ports": [ { "port": "9200", "protocol": "TCP" } ] } ]
}
],
"enableDefaultDeny": { "ingress": true, "egress": true }
}
Safe rollout plan
Rolling out network policy incrementally avoids accidental disruption. Use this checklist to introduce policy in a controlled way:
- Observe with Hubble. Start by running
hubble observein the target namespace to see which pods talk to each other and to external services. Map the flows and note DNS queries and external endpoints. - Create a baseline policy inside the namespace. Apply a CiliumNetworkPolicy that allows ingress and egress between all pods in the namespace. This preserves current behaviour while you start tightening rules.
- Add platform allowances. Explicitly allow DNS egress to kube-dns and access to the Kubernetes API server so platform features such as service discovery, leader election and health checks continue to work.
- Introduce narrow allows one at a time. Use labels, entities, names, and CIDR blocks to allow only what each workload needs. Add one service at a time and verify with Hubble before moving on.
- Enable default deny for that subject. Once an allow list exists for a direction on a workload, default deny applies. Confirm with Hubble that unwanted flows are now denied and only your allows remain.
- Add application layer rules selectively. Use HTTP or DNS rules for the few cases where you need method or path control. Keep the rest at Layer 4 for simplicity and performance.
Tip: You can run policies in audit posture during discovery. Verdicts are recorded without blocking traffic, letting you refine rules safely. When you are confident, switch to enforcement and Hubble will immediately show which flows are allowed, denied, or audited.
Further reading
Two ebooks which I highly recommend getting your hands on!
- Cilium Network Policy Deep Dive, an extensive guide to identity, selectors, entities, policy actions, datapath and operations.
- Kubernetes Network Policies Done the Right Way, an adoption guide with strategy, audit posture, and Hubble workflows.
Reference links
- Cilium Security Policy overview
- Cilium policy language and selectors
- DNS based policy
- Hubble observability
- Host firewall
By following this guide you now have a mental model of how labels, identities and rules fit together, plus working YAML examples you can adapt to your own environment. Start small, observe real flows with Hubble, and tighten policy step by step.
The Cilium documentation stays up to date with the exact syntax for your version, so keep it open as you experiment. With this approach you can bring workloads under policy control safely and predictably, one service at a time, and gain visibility and enforcement without disruption.
Regards