NetworkPolicy#
When we built custom bridge networks in Docker, the goal was isolation: containers on network A can’t see containers on network B. NetworkPolicy is the cluster-level equivalent - with a twist that trips everyone up at first.
The default is “allow everything”. No policy = every pod can talk to every pod, everywhere. But the moment a pod is selected by any NetworkPolicy (Ingress or Egress, even an empty one), that flips: all traffic to/from that pod is denied unless another policy explicitly allows it. Policies are additive: the union of all rules selecting a pod defines its allowed traffic. There is no “deny” rule - you deny by omission.
policyTypes: [Ingress]- only controls who may send traffic to the selected podspolicyTypes: [Ingress, Egress]- also controls what the selected pods may reachpodSelector: {}- selects all pods in the namespace- An empty
ingress:list = allow nothing (that’s your default-deny)
Selector Semantics: AND vs OR#
The from field mixes two selector types, and their combination rules are subtle:
ingress:
- from:
- podSelector:
matchLabels:
app: web
- namespaceSelector:
matchLabels:
team: backendTwo separate items in the list = OR: pods labeled app: web from any namespace, plus any pod from the team: backend namespace. That’s almost never what you meant.
ingress:
- from:
- namespaceSelector:
matchLabels:
team: backend
podSelector:
matchLabels:
app: webTwo selectors in the same item = AND: pods labeled app: web that live in namespaces labeled team: backend. Same YAML elements, one less space of indentation - completely different rule.
Egress’te ise selector’ların beraberinde DNS’i düşünmek zorunlu. Egress’i kısıtladığın pod, CoreDNS’e ulaşamazsa her domain çözümlemesi ölür. Bu yüzden her egress policy’sine
kube-dns‘e53/UDP+TCPizni eklemek standart pratiktir.
ipBlock, Ports and Named Ports#
from can also be a CIDR:
from:
- ipBlock:
cidr: 192.168.1.0/24
except:
- 192.168.1.20/32
ipBlockmatches pod IPs, which are virtual and rescheduled freely. It’s appropriate for pods that run on the node host itself, external VMs or (in some setups) node IPs - not for “that other deployment”. Use label selectors between pods; save CIDRs for genuinely external sources.
Ports can be numeric or - better - named, matching the service/container port names:
ports:
- port: http
protocol: TCPRenaming a numeric port later breaks policies silently; named ports keep the policy meaningful. But check your CNI supports named ports (calico and cilium do; some older ones don’t).
The CNI Reality Check#
NetworkPolicy is just an API. Enforcement happens in the CNI plugin, and that’s where most homelab confusion lives:
- flannel: implements nothing - policies exist, are ignored
- calico: full enforcement, its own iptables programs
- kube-router, cilium: enforce as well (cilium adds richer eBPF-based policies beyond the standard API)
Our multi-node cluster runs Cilium - everything in this note is actually enforced there, no asterisks. On Minikube’s default CNI nothing is enforced; to test anything here, start the cluster policy-aware:
minikube start --cni=calico
# or: minikube start --cni=cilium
# or: minikube addons enable network-policyHow to Build a Default-Deny Zone#
Make sure the cluster’s CNI actually enforces (see above).
Apply the namespace-wide deny:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingresskubectl apply -f default-deny-ingress.yaml- Prove it works - a pod outside your allowed sources should time out, not get an error page:
kubectl run nettest --rm -it --image=cilium/netshoot:latest -- curl --max-time 3 http://api-service:8080Timeout (not connection refused) is the signature of a dropped packet at policy level. Connection refused would mean the pod answered - i.e. your policy isn’t being enforced at all.
- Check what k8s thinks the policy means:
kubectl describe networkpolicy default-deny-ingressHow to Allow Only web → api Traffic#
- Write the allow rule with the AND-semantics you actually mean:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-api
spec:
podSelector:
matchLabels:
app: synergychat-api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: synergychat-web
ports:
- port: 8080
protocol: TCP- Apply, then test the allowed path from a properly labeled pod -
kubectl run busyboxcreates an unlabeled pod, so label it at creation:
kubectl run nettest --rm -it --image=cilium/netshoot:latest \
--labels="app=synergychat-web" -- curl --max-time 3 http://api-service:8080Test the denied path: the same curl without
--labelsmust now fail. That asymmetry (labeled works, unlabeled times out) proves the policy is enforcing, not just present.Roll out the same pattern to the crawler namespace: default-deny there, then allow
api → crawleron the crawler’s port only.
How to Verify Policies with netshoot#
cilium/netshoot is a Swiss-army network toolbox container. A few checks worth scripting:
- Confirm DNS still resolves (egress policy sanity):
kubectl run dnstest --rm -it --image=cilium/netshoot:latest -- nslookup api-service- Enumerate what a pod is actually allowed - kubectl has no “effective policy” view, so check by source:
kubectl get networkpolicy -o wide
kubectl get netpol allow-web-to-api -o yaml | grep -A8 "ingress:"- Delete the allow policy and watch the allowed path break, then restore it. Ending on a positive control is what turns “I think it works” into “it demonstrably works”.
for more security-context