Container network security means controlling exactly which workloads can reach each other over the network and denying everything else by default. In a default Kubernetes cluster, every pod can reach every other pod on any port. That flat network is convenient for developers and a gift to anyone who lands a foothold in one container, because lateral movement is free. Getting container network security right is mostly about replacing that implicit trust with explicit, auditable rules.
This guide walks through the controls that actually matter: segmentation, default-deny policies, encrypted service-to-service traffic, and egress filtering. The examples target Kubernetes because that is where most teams run containers, but the principles apply to plain Docker networks and managed services too.
Why the flat network model is the core problem
When you deploy a fresh cluster, the CNI plugin gives every pod an IP and lets it route to every other pod. A compromised frontend can open a socket to your payments database, your internal admin API, and the Kubernetes API server. Attackers know this. The 2018 Tesla cluster incident and countless cryptomining campaigns since then all followed the same pattern: get into one container, then pivot across the open network.
The fix is segmentation. You want traffic to flow only along the paths your architecture actually requires. A web tier should reach its API tier and nothing else. A batch job should reach its queue and its object store, not the user database. Everything not on the allowlist should be dropped.
Start with a default-deny NetworkPolicy
Kubernetes NetworkPolicy is the built-in mechanism, but it only takes effect if your CNI supports it (Calico, Cilium, and Antrea do; the basic flannel plugin does not). The single most valuable policy you can apply is a namespace-wide default deny for ingress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
An empty podSelector matches every pod in the namespace, and because no ingress rules are listed, all inbound traffic is denied. From there you add narrow allow rules. This policy lets the API tier accept traffic only from the web tier on port 8080:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-api
namespace: payments
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 8080
Apply default-deny first, then add allows, and test each service after each change. Doing it the other way around leaves gaps open while you iterate.
Do not forget egress
Ingress rules stop attackers from reaching your workloads. Egress rules stop a compromised workload from reaching the internet, which is how data gets exfiltrated and how malware phones home to command-and-control servers. A container that only needs to talk to an internal database has no business making outbound calls to an unknown host.
Add an egress policy that permits DNS and your known dependencies, and denies the rest:
spec:
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
Egress control is also your cleanest defense against dependency-confusion and supply chain attacks that try to beacon out during a build or at runtime. If the workload physically cannot reach an arbitrary host, a malicious package has nowhere to send stolen tokens.
Encrypt service-to-service traffic with mTLS
Network policy controls who can connect. It does not encrypt the bytes on the wire or verify workload identity cryptographically. For that, a service mesh such as Istio or Linkerd gives you mutual TLS between pods, where each side presents a certificate tied to its service identity. This matters in multi-tenant clusters and anywhere traffic crosses a node boundary you do not fully trust.
You do not always need a full mesh. If your requirements are modest, terminating TLS at the ingress gateway and using NetworkPolicy internally may be enough. Reach for mTLS when you need strong workload identity, encryption in transit as a compliance requirement, or fine-grained authorization based on the calling service rather than its IP.
Harden the container image and runtime too
Network security is one layer. It works best alongside the others. A pod that runs as root with a writable filesystem and the full Linux capability set is far more useful to an attacker who gets in, even if the network is locked down. Drop capabilities, set runAsNonRoot: true, use a read-only root filesystem, and pin images by digest rather than a mutable tag.
The images themselves are the other half. A base image full of known CVEs is a network problem waiting to happen, because remote code execution in an exposed service is what gives an attacker the foothold in the first place. Scanning images before they ship is table stakes; our software composition analysis page covers how transitive dependency vulnerabilities get surfaced. An SCA tool such as Safeguard can flag a vulnerable library in a base image before it reaches a running pod.
Observe, log, and test your policies
Policies you cannot see are policies you cannot trust. Enable flow logging in your CNI (Cilium's Hubble and Calico's flow logs both do this) so you can see which connections are allowed and denied. Denied-flow logs are a live signal: a spike of drops toward the database from an unexpected pod is either a misconfiguration or an intrusion, and you want to know which.
Test policies the way you test code. A simple check is to kubectl exec into a pod that should not have access and try to reach a restricted service:
kubectl exec -it web-pod -n payments -- \
curl -m 3 http://database.payments.svc:5432
If that connection times out, your default-deny is working. Fold these checks into CI so a well-meaning policy edit cannot quietly reopen a path. For teams building this discipline from scratch, the Safeguard Academy has walkthroughs on segmentation patterns.
FAQ
Does Kubernetes enforce NetworkPolicy on its own?
No. NetworkPolicy is an API object, but enforcement is delegated to the CNI plugin. Calico, Cilium, and Antrea enforce it; the stock flannel plugin ignores it entirely. Confirm your CNI supports policy before you rely on it, or your rules will silently do nothing.
What is the difference between NetworkPolicy and a service mesh?
NetworkPolicy operates at layers 3 and 4, controlling which IPs and ports can connect. A service mesh works at layer 7, adding mutual TLS, identity-based authorization, and request-level routing. They are complementary: use NetworkPolicy for coarse segmentation and a mesh when you need encryption and workload identity.
How do I roll out default-deny without breaking production?
Apply it in a staging namespace first, watch the denied-flow logs, and add allow rules until traffic flows normally. Then promote the same policy set to production during a low-traffic window and keep the flow logs open so you can spot anything you missed.
Is egress filtering really necessary?
Yes, if you care about data exfiltration and malware callbacks. Ingress rules protect you from inbound attacks, but egress rules are what stop a compromised container from shipping your secrets to an attacker-controlled server or downloading a second-stage payload.