Network Policies (Cilium)
Control pod-to-pod, ingress, and egress traffic on your production cluster with Cilium.
Your production cluster uses Cilium as its CNI (and as its Gateway API implementation).
Cilium enforces both standard Kubernetes NetworkPolicy and its own richer
CiliumNetworkPolicy, which adds Layer-7 (HTTP), DNS-based egress, and named “entities”.
# How isolation works (important)
Kubernetes networking is allow-by-default until a pod is selected. A pod is unselected
(all traffic allowed) until at least one policy’s podSelector matches it. The moment a
policy selects it, that pod becomes isolated for that direction (ingress and/or egress),
and from then on only what your policies explicitly allow is permitted.
Two consequences worth internalising:
- Adding your first ingress policy to a pod doesn’t just “add a rule”; it flips that pod to deny-all-ingress-except-what-you-list.
ingressandegressare independent. Selecting a pod for ingress does nothing to its egress, and vice versa.
If you want a hard “deny everything unless allowed” baseline for a namespace, apply the default-deny policy below, then layer allow-rules on top.
# Recipe 1: allow traffic from another namespace
Let pods in namespace-b receive traffic from pods in namespace-a:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-namespace-a
namespace: namespace-b
spec:
podSelector: {} # all pods in namespace-b
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: namespace-a
kubernetes.io/metadata.nameis a label Kubernetes sets automatically on every namespace, so you can select namespaces by name without labelling them yourself.
# Recipe 2: allow only a specific app to talk to another
Allow only pods labelled app: web (in frontend) to reach pods labelled app: api on
port 8080:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-web
namespace: api
spec:
podSelector:
matchLabels:
app: api
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend
podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 8080
# Establishing a default-deny baseline
Apply this to a namespace to deny all ingress and egress for every pod, then add explicit allows. (You almost always want to also allow DNS; see the next recipe.)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: my-namespace
spec:
podSelector: {} # selects every pod
policyTypes: [Ingress, Egress] # ...for both directions, with no allow rules => deny
# Recipe 3: allow DNS egress (almost always needed)
A default-deny-egress namespace can’t resolve DNS, which breaks nearly everything. Allow it:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: my-namespace
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Exposing a service to the internet
External traffic does not arrive via NetworkPolicy; it comes through the Gateway API
(implemented by Cilium). On your production cluster you create your own Gateway, then a
service is only reachable from the internet if it has an HTTPRoute attached to that
Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
namespace: my-namespace
spec:
parentRefs:
- name: app-gateway # the Gateway you created
namespace: my-namespace
hostnames:
- "my-app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: my-app
port: 80
See Exposing Apps (Gateway API) for the full setup — creating the Gateway, TLS, listener ports, and DNS.
Without an HTTPRoute, a Service is reachable only inside the cluster. If you’ve enabled
default-deny-ingress, also add an ingress allow for traffic coming from the gateway so the
proxy can reach your pods.
# Cilium-specific power features
When plain NetworkPolicy isn’t enough, use CiliumNetworkPolicy.
# Restrict egress to specific external hostnames (DNS-aware)
Lock a workload down so it can only reach, say, the Stripe API:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-egress-allowlist
namespace: api
spec:
endpointSelector:
matchLabels:
app: api
egress:
# DNS must be allowed, and Cilium watches these lookups to enforce toFQDNs
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: ANY
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "api.stripe.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
# Allow egress to the internet but nothing internal
Cilium “entities” let you reference logical groups like world (everything outside the
cluster) or cluster:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: web-egress-world-only
namespace: frontend
spec:
endpointSelector:
matchLabels:
app: web
egress:
- toEntities:
- world
# Layer-7 (HTTP) rules
Cilium can allow only specific HTTP methods/paths, e.g. let a client GET from the API but
not POST:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-readonly-from-frontend
namespace: api
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/v1/.*"
# Verifying your policies
# List policies in a namespace
kubectl get networkpolicy,ciliumnetworkpolicy -n <namespace>
# Inspect what a policy matches
kubectl describe networkpolicy <name> -n <namespace>
To observe live allow/deny decisions, ask your TrueFullstaq engineer about Hubble, the Cilium observability layer, which can show flow-by-flow whether traffic was forwarded or dropped, which is invaluable when a policy is stricter than you intended.
Tip: roll out policies incrementally. Start permissive, confirm your app works, then add default-deny and re-test, so a dropped flow is easy to attribute to the policy you just added.