NetworkPolicies
Pod-level firewalling in Kubernetes, templated through Helm.
Why NetworkPolicies Exist
By default, every pod in a Kubernetes cluster can communicate with every other pod on any port, across any namespace. This flat network model makes development easy but is a security liability in production. A compromised pod can freely probe databases, caches, and internal APIs it should never touch.
NetworkPolicies solve this by acting as a declarative firewall attached to pods. They let you express rules like "only the frontend pods may reach the API on port 8080" or "nothing in this namespace is allowed to make outbound connections except to DNS." Unlike traditional firewalls that operate at the IP level, NetworkPolicies are label-aware: rules follow pods as they scale, restart, and get new IPs.
Key mental model
Without any NetworkPolicy objects in a namespace, all traffic is allowed. The moment you create a policy that selects a pod, that pod switches to a default-deny posture for the direction(s) the policy covers. You then add explicit allow rules.
Default-Deny Patterns
The first step toward a zero-trust network is to create a default-deny policy. These are minimal policies that select all pods (via an empty podSelector) and declare a policy type without providing any allow rules.
Deny All Ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
Every pod in the production namespace now rejects all inbound traffic unless another NetworkPolicy explicitly allows it.
Deny All Egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
Deny Both Directions
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Warning
Denying all egress breaks DNS resolution. Pods will be unable to resolve service names. You almost always need a companion rule that allows UDP/TCP port 53 egress.
Ingress Rule Anatomy
An ingress rule describes who is allowed to send traffic into the selected pods and on which ports. Each rule has a from array (the source selectors) and an optional ports array.
There are three types of source selectors:
| Selector | Matches | Example |
|---|---|---|
podSelector | Pods in the same namespace by label | app: frontend |
namespaceSelector | All pods in namespaces matching labels | env: production |
ipBlock | CIDR ranges, with optional exceptions | 10.0.0.0/8 except 10.0.1.0/24 |
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 8443
This policy applies to pods labeled app: api and allows inbound TCP traffic on ports 8080 and 8443, but only from pods labeled app: frontend in the same namespace.
Egress Rule Anatomy
Egress rules mirror ingress rules but control outbound traffic. The to array accepts the same three selector types: podSelector, namespaceSelector, and ipBlock.
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow outbound to the database
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Allow external HTTPS
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
Note the DNS rule uses an empty to: [], which means "any destination" but is restricted to port 53 only. This is the standard pattern for allowing DNS while keeping egress locked down.
AND vs OR: The Selector Trap
This is the single most misunderstood aspect of NetworkPolicies. The difference between AND and OR comes down to whether two selectors are in the same list item or separate list items.
OR logic (two list items)
from:
- podSelector: # item 1
matchLabels:
app: frontend
- namespaceSelector: # item 2
matchLabels:
env: staging
This allows traffic from pods labeled app: frontend in any namespace, OR from any pod in namespaces labeled env: staging. These are two independent conditions.
AND logic (one list item)
from:
- podSelector: # same item
matchLabels:
app: frontend
namespaceSelector: # same item (no dash)
matchLabels:
env: staging
This allows traffic only from pods labeled app: frontend that are also in namespaces labeled env: staging. Both conditions must be true.
Tip
Always render your templates with helm template and carefully inspect the YAML dashes in the from/to arrays. A misplaced dash can silently widen your policy to allow unintended traffic.
Common Patterns
Allow only same-namespace traffic
ingress:
- from:
- podSelector: {}
An empty podSelector matches all pods, but only within the policy's own namespace (since there is no namespaceSelector).
Allow from a specific namespace
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
The built-in label kubernetes.io/metadata.name matches a namespace by its actual name, which is more precise than custom labels.
Allow DNS egress (always include this)
egress:
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Templating NetworkPolicies in Helm
NetworkPolicies are excellent candidates for Helm templating because different environments have different security requirements. A development cluster might disable them entirely, while production enforces strict rules.
# templates/networkpolicy.yaml
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
policyTypes:
{{- range .Values.networkPolicy.policyTypes }}
- {{ . }}
{{- end }}
{{- with .Values.networkPolicy.ingress }}
ingress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.networkPolicy.egress }}
egress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
# values.yaml
networkPolicy:
enabled: false
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to: []
ports:
- protocol: UDP
port: 53
# values-production.yaml (override)
networkPolicy:
enabled: true
Best practice
Use {{- with }} instead of {{- if }} for the ingress/egress blocks. This avoids rendering an empty ingress: key when no rules are defined, which would produce an invalid policy.
CNI Requirements
NetworkPolicy is a Kubernetes API object, but the actual enforcement depends on your cluster's CNI (Container Network Interface) plugin. The API server will always accept the resource; a missing CNI implementation simply means the rules are silently ignored.
| CNI Plugin | NetworkPolicy Support | Notes |
|---|---|---|
| Calico | Full | Most widely deployed; also supports its own CRDs for extended policies |
| Cilium | Full + L7 | Supports HTTP-aware policies (CiliumNetworkPolicy CRD) |
| Weave Net | Full | Straightforward setup |
| Flannel | None | Policies are accepted but never enforced |
| AWS VPC CNI | Requires addon | Install Calico as a policy-only addon alongside VPC CNI |
| Azure CNI | Full (with Azure NPM) | Enable network policy engine during cluster creation |
Critical
If you deploy NetworkPolicies on a cluster running Flannel, nothing is enforced. Your manifests will exist in the API but provide zero security. Always verify your CNI before relying on NetworkPolicies.
Testing and Debugging
NetworkPolicies are notoriously hard to debug because there are no logs by default -- connections are simply dropped. Here is a systematic approach:
1. Verify policies are applied
# List all policies in the namespace
kubectl get networkpolicy -n myns
# Inspect a specific policy
kubectl describe networkpolicy allow-frontend -n myns
2. Test connectivity from inside the cluster
# Exec into an existing pod
kubectl exec -it deploy/frontend -n myns -- \
curl -s --max-time 3 http://api:8080/health
# Spin up a temporary test pod
kubectl run nettest --rm -it \
--image=busybox:1.36 \
--restart=Never -n myns -- \
wget -qO- --timeout=3 http://api:8080/health
3. Check from a different namespace
# Test cross-namespace connectivity
kubectl run nettest --rm -it \
--image=busybox:1.36 \
--restart=Never -n other-ns -- \
wget -qO- --timeout=3 http://api.myns.svc:8080/health
4. Common debugging checklist
- Is the CNI plugin actually enforcing policies? (Check with a known-blocked connection.)
- Does
podSelectormatch the target pods? (Check labels withkubectl get pods --show-labels.) - Are the ports correct? (A mismatch between policy port and container port silently blocks traffic.)
- Did you forget to allow DNS egress? (Service names will not resolve.)
- Is the policy in the correct namespace? (NetworkPolicies are namespace-scoped.)
Calico tip
Enable flow logs with calicoctl to see which packets are being allowed or denied. Cilium provides similar observability via hubble.