Network policies
Cluster traffic — click any arrow
Why?
ALLOWEDThe frontend calls the API.
A connection succeeds only if both ① and ② allow it. Replies on an allowed connection are let through automatically (policies are stateful).
Policies in effect
- isolates
- allows ✓
- isolatesfrom:
YAML · default-deny-ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: shop
spec:
podSelector: {} # every pod in "shop"
policyTypes: [Ingress] # ...is now isolated for ingress
# no ingress rules => nothing is allowed inDefault deny, then punch holes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: shop
spec:
podSelector: {} # all pods in the namespace
policyTypes: [Ingress, Egress] # isolate both directions, allow nothing
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: shop
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}# list what's in force
kubectl get networkpolicy -A
kubectl describe networkpolicy -n shop allow-frontend-to-api
# probe from a throwaway pod that carries the frontend's labels
kubectl run probe -n shop --rm -it --restart=Never \
--labels app=frontend --image=busybox:1.36 -- \
sh -c 'nslookup api.shop.svc.cluster.local && wget -qO- -T 2 http://api:8080/healthz'
# with Cilium + Hubble: watch the drops live, with the reason
hubble observe --namespace shop --verdict DROPPED --followThe namespaceSelector / podSelector gotcha
One dash, very different policy
# policy lives in namespace "shop"
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector: # same list item (no dash)
matchLabels:
app: prometheus1 pods may reach the API. One peer with both selectors: the pod must be in a namespace labelled monitoring AND carry app=prometheus. Exactly one pod matches.
Beyond NetworkPolicy
Evaluation order: admin tiers around NetworkPolicy
- 1. AdminNetworkPolicycluster admin · priority-ordered · Allow / Deny / Pass
No AdminNetworkPolicy rule matches — fall through.
- 2. NetworkPolicynamespace owner · allow-list only
No NetworkPolicy selects the pod for this direction — it is not isolated, fall through.
- 3. BaselineAdminNetworkPolicycluster admin · singleton 'default' · Allow / Deny
BaselineAdminNetworkPolicy rule matches with Deny — dropped. This is the cluster-wide 'default deny unless the team opts in' guardrail.
- 4. defaultallow everything
- → DROPPED
apiVersion: policy.networking.k8s.io/v1alpha1
kind: AdminNetworkPolicy
metadata:
name: always-allow-monitoring
spec:
priority: 10 # lower number = evaluated first
subject:
namespaces: {} # applies to every namespace
ingress:
- name: prometheus-scrapes-everything
action: Allow # tenants can't block it
from:
- pods:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
---
apiVersion: policy.networking.k8s.io/v1alpha1
kind: BaselineAdminNetworkPolicy
metadata:
name: default # singleton
spec:
subject:
namespaces:
matchLabels:
tenant: "true"
ingress:
- name: deny-cross-namespace-by-default
action: Deny # applies only where no NetworkPolicy decides
from:
- namespaces: {}apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api
namespace: shop
spec:
endpointSelector:
matchLabels: {app: api}
ingress:
- fromEndpoints:
- matchLabels: {app: frontend}
toPorts:
- ports: [{port: "8080", protocol: TCP}]
rules:
http: # L7: only these requests pass
- method: GET
path: "/products.*"
- method: POST
path: "/orders"
egress:
- toEndpoints: # DNS, and let Cilium's proxy see the lookups
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports: [{port: "53", protocol: ANY}]
rules:
dns:
- matchPattern: "*"
- toFQDNs: # egress by hostname, not IP
- matchName: api.stripe.com
toPorts:
- ports: [{port: "443", protocol: TCP}]CNI-native policy
Cilium adds CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy: L7 rules (HTTP method/path, gRPC, Kafka, DNS) enforced by its Envoy proxy, toFQDNs egress that follows DNS answers instead of hard-coded IPs, explicit deny rules, and entities like world, host, kube-apiserver.
Calico adds NetworkPolicy and GlobalNetworkPolicy (projectcalico.org/v3) with an order, and actions Allow / Deny / Log / Pass, plus host-endpoint protection.
Trade-off: richer semantics, but your policies are now tied to one CNI. Start with portable NetworkPolicy and reach for the CRDs when you need L7, FQDNs or admin guardrails.
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: deny-metadata-and-smtp
spec:
order: 100 # explicit ordering, explicit Deny
selector: all()
types: [Egress]
egress:
- action: Deny
protocol: TCP
destination:
ports: [25]
- action: Deny
destination:
nets: [169.254.169.254/32]
- action: Pass # hand over to namespaced policiesOn Talos
# Talos ships Flannel by default — it does NOT enforce NetworkPolicy.
# Disable it (and kube-proxy, if Cilium replaces it) and install a policy-capable CNI.
cluster:
network:
cni:
name: none
proxy:
disabled: true- why it mattersA pod is open until some policy selects it for a direction; then it's closed except for the union of allows. Both the sender's egress and the receiver's ingress must permit a connection.
- why it mattersDefault-deny egress breaks DNS first. Always ship an allow-dns policy alongside it, on UDP and TCP 53.
- why it mattersPolicies are enforced by the CNI, not Kubernetes. Talos' default Flannel ignores them — run Cilium or Calico, and test that a deny really drops traffic.