| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Bash-compatible Linux or macOS shell with kubectl configured; Kubernetes 1.27+ cluster with a NetworkPolicy-capable CNI |
| Cert prep | CKA · CKAD · CKS |
| Lab environment | Multi-node kubeadm cluster with containerd and Calico — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Kubernetes permissions | Create Namespaces, Deployments, Services, and NetworkPolicies; get/list/watch Pods, Deployments, Namespaces, and NetworkPolicies; patch and delete NetworkPolicies; and exec into Pods |
| Scope | networking.k8s.io/v1 NetworkPolicy allow-list model, default-deny ingress and egress, podSelector, namespaceSelector, AND versus OR selector combinations, port rules, DNS egress, bidirectional client-to-server policies, and ipBlock basics. Assumes working cluster DNS and Services. Does not cover CNI installation, layer-7 HTTP policies, FQDN egress, service mesh authorization, host firewall rules, or vendor policy CRDs. |
| Related guides | Troubleshoot a Service that is not working Labels and selectors |
NetworkPolicy is how you restrict which Pods can talk to each other inside a cluster. This walkthrough uses three namespaces — backend, frontend, and untrusted — plus one backend server and three client Deployments. You will confirm open connectivity first, apply default-deny rules, then restore only the paths you intend.
Understand Kubernetes NetworkPolicy
Confirm CNI Enforcement
The Kubernetes API accepts NetworkPolicy objects even when nothing enforces them. If you apply a deny policy and traffic behaviour does not change, suspect a CNI that ignores policies or enforcement that is disabled on your platform. Confirm how your CNI and kube-proxy fit into cluster networking in Kubernetes networking before you treat policy YAML as a security control.
This article was tested on a kubeadm cluster running Calico, which enforces NetworkPolicy. Confirm your CNI supports policy before relying on the examples below. Do not treat policy YAML as a security control until you verify enforcement on your own cluster.
List Calico node agents (your CNI labels may differ):
kubectl get pods -n calico-system -l k8s-app=calico-nodeExample output from the tested Calico cluster; Pod names, restart counts, and ages vary:
NAME READY STATUS RESTARTS AGE
calico-node-hc7ph 1/1 Running 1 (39h ago) 2d10h
calico-node-sc7p2 1/1 Running 0 24hRunning node agents on every worker are a good sign, but the definitive test is behavioural: apply a deny policy and confirm traffic actually stops.
Isolation and the Additive Allow-List Model
NetworkPolicy is not a firewall rule list evaluated top to bottom. It uses an allow-list model:
- Pods are non-isolated by default — all ingress and egress is permitted until a policy selects them.
- A policy that selects a Pod for ingress isolates that Pod's ingress. Only explicitly allowed sources may connect.
- A policy that selects a Pod for egress isolates that Pod's egress. Only explicitly allowed destinations may be reached.
- Policies add permitted traffic. They are not ordered; multiple policies combine additively.
- For a connection to succeed when both ends are isolated, the source needs egress permission and the destination needs ingress permission for the same flow.
Non-isolated Pod → all traffic allowed
Ingress-isolated Pod → only listed ingress peers/ports allowed
Egress-isolated Pod → only listed egress peers/ports allowedWhen several policies select the same Pod, traffic is allowed if any applicable policy permits it. Delete or replace earlier allow rules before testing a later policy, or a leftover rule can make the next test succeed for the wrong reason.
Protocol and Node-Traffic Limitations
Standard NetworkPolicy rules define behaviour for TCP, UDP, and SCTP traffic. ICMP, ARP, and other protocol behaviour can differ between network plugins. A Pod cannot use NetworkPolicy to block itself, and traffic to and from the node hosting the Pod is always allowed.
Build and Verify the Baseline Lab
Create three namespaces with meaningful labels, a backend server in backend, an allowed client in frontend, and an untrusted client in untrusted. A local-client Deployment in backend supports the same-namespace podSelector example later.
apiVersion: v1
kind: Namespace
metadata:
name: backend
labels:
app: backend
---
apiVersion: v1
kind: Namespace
metadata:
name: frontend
labels:
access: allowed
---
apiVersion: v1
kind: Namespace
metadata:
name: untrusted
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: backend
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
role: server
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0.0
args: ["-listen=:8080", "-text=backend-ok"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: backend
spec:
selector:
app: backend
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
namespace: frontend
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
role: client
spec:
containers:
- name: curl
image: curlimages/curl:8.12.1
command: ["sleep", "3600"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
namespace: untrusted
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
role: client
spec:
containers:
- name: curl
image: curlimages/curl:8.12.1
command: ["sleep", "3600"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: local-client
namespace: backend
spec:
replicas: 1
selector:
matchLabels:
app: local-client
template:
metadata:
labels:
app: local-client
role: client
spec:
containers:
- name: curl
image: curlimages/curl:8.12.1
command: ["sleep", "3600"]Apply the lab manifest:
kubectl apply -f networkpolicy-lab.yamlWait for every Deployment to become available:
kubectl wait --for=condition=Available deployment --all -n backend --timeout=120s
kubectl wait --for=condition=Available deployment --all -n frontend --timeout=120s
kubectl wait --for=condition=Available deployment --all -n untrusted --timeout=120sRepresentative output:
deployment.apps/backend condition met
deployment.apps/local-client condition met
deployment.apps/client condition met
deployment.apps/client condition metTest cross-namespace access from frontend using the backend Service DNS name. HTTP probes from client Pods use the curl command with --connect-timeout so a blocked path fails fast instead of hanging. A trailing dot on the FQDN avoids search-domain ambiguity on some clusters:
kubectl -n frontend exec deploy/client -- curl -s --connect-timeout 5 http://backend.backend.svc.cluster.local.:8080Sample output:
backend-okRepeat from the untrusted namespace:
kubectl -n untrusted exec deploy/client -- curl -s --connect-timeout 5 http://backend.backend.svc.cluster.local.:8080Sample output:
backend-okTest the in-namespace short name from local-client:
kubectl -n backend exec deploy/local-client -- curl -s --connect-timeout 5 http://backend:8080Sample output:
backend-okAll three clients reach the backend before any policy is applied. Record this baseline — you will compare every later step against it.
Configure Ingress Policies
Default-Deny Ingress
A default-deny ingress policy selects every Pod in a namespace and declares ingress isolation with no allow rules.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: backend
spec:
podSelector: {}
policyTypes:
- IngressApply it to backend:
kubectl apply -f default-deny-ingress.yamlAn empty podSelector: {} matches all Pods in the policy namespace. With policyTypes: [Ingress] and no ingress allow list, the policy allows no new TCP, UDP, or SCTP ingress connections except Pod self-traffic and traffic from the node hosting the Pod, unless another applicable policy permits the connection. Egress is unaffected.
NetworkPolicy has no portable readiness condition. A policy object has no dataplane-ready status; effects are implemented by the CNI. Each probe opens a new connection and retries briefly while the CNI programs the updated policy.
When traffic should be blocked, use:
blocked=false
for attempt in {1..20}; do
code=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}" \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$code" == "000" ]]; then
printf '%s\n' "$code"
blocked=true
break
fi
sleep 1
done
if [[ "$blocked" != "true" ]]; then
echo "Traffic was not blocked; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
000When a policy change should restore access, use the same bounded structure with allowed=false and exit if the success condition is not met within 20 attempts.
A 000 response from curl means the connection never completed — ingress to the backend is blocked. The tested HTTP/TCP requests from the untrusted client and local-client fail the same way:
kubectl \
-n untrusted exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend.backend.svc.cluster.local.:8080 || true'kubectl \
-n backend exec deploy/local-client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend:8080 || true'Sample output:
000Use the same bounded retry on each blocked probe below while the CNI converges.
Allow Same-Namespace Pods
podSelector in a from clause matches Pods only in the same namespace as the NetworkPolicy. Add a rule that allows ingress to backend servers from client Pods in backend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-client-ingress
namespace: backend
spec:
podSelector:
matchLabels:
role: server
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: client
ports:
- protocol: TCP
port: 8080Apply the allow policy while the default-deny policy remains in place:
kubectl apply -f allow-client-ingress.yamlRetry until the same-namespace client succeeds:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl -n backend exec deploy/local-client -- sh -c 'curl -s --connect-timeout 2 http://backend:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okThe frontend client in another namespace is still blocked because podSelector alone does not match cross-namespace Pods:
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend.backend.svc.cluster.local.:8080 || true'Sample output:
000After completing the same-namespace podSelector test, remove the pod-only allow rule:
kubectl delete networkpolicy allow-client-ingress -n backendAllow a Selected Namespace
Permit ingress from namespaces labeled access: allowed:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-ns
namespace: backend
spec:
podSelector:
matchLabels:
role: server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
access: allowed
ports:
- protocol: TCP
port: 8080namespaceSelector matches namespaces by label, not by writing a namespace name as a plain string. To select a namespace by its Kubernetes name on current clusters, use the built-in label kubernetes.io/metadata.name.
Apply and test the namespace rule:
kubectl apply -f allow-frontend-ns.yamlRetry until the frontend client succeeds:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okThe untrusted namespace has no matching label, so its client remains blocked:
kubectl \
-n untrusted exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend.backend.svc.cluster.local.:8080 || true'Sample output:
000Restrict Ports and Protocols
ports under an ingress or egress rule filters destination ports on the selected Pods:
ports:
- protocol: TCP
port: 8080Omitting ports allows all ports for peers matched by that rule. Named ports are supported when the policy implementation resolves them on the selected Pods.
While allow-frontend-ns is active, change its allowed port to 80 even though the application listens on 8080:
kubectl patch networkpolicy allow-frontend-ns -n backend --type=json -p='[
{
"op": "replace",
"path": "/spec/ingress/0/ports/0/port",
"value": 80
}
]'Sample output:
networkpolicy.networking.k8s.io/allow-frontend-ns patchedRun the blocked curl test:
blocked=false
for attempt in {1..20}; do
code=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}" \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$code" == "000" ]]; then
printf '%s\n' "$code"
blocked=true
break
fi
sleep 1
done
if [[ "$blocked" != "true" ]]; then
echo "Traffic was not blocked; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
000Restore the correct port:
kubectl patch networkpolicy allow-frontend-ns -n backend --type=json -p='[
{
"op": "replace",
"path": "/spec/ingress/0/ports/0/port",
"value": 8080
}
]'Retry until access is restored:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okA Service port can differ from the Pod targetPort. Write policies against the port the dataplane observes on the destination Pod — validate with direct Pod tests when results look wrong. The optional endPort field defines a port range when you need more than one consecutive port.
After the namespace and port tests:
kubectl delete networkpolicy allow-frontend-ns -n backendCombine Namespace and Pod Selectors
Indentation under from and to changes whether selectors combine with AND or OR. This is one of the most common NetworkPolicy mistakes.
Same from item: AND
Both selectors under one list item must match:
from:
- namespaceSelector:
matchLabels:
access: allowed
podSelector:
matchLabels:
role: clientThis means: Pods with role=client inside namespaces labeled access=allowed.
Apply and test the AND rule:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-and-rule
namespace: backend
spec:
podSelector:
matchLabels:
role: server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
access: allowed
podSelector:
matchLabels:
role: client
ports:
- protocol: TCP
port: 8080kubectl apply -f allow-and-rule.yamlRetry until the frontend client succeeds:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okkubectl \
-n untrusted exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend.backend.svc.cluster.local.:8080 || true'kubectl \
-n backend exec deploy/local-client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend:8080 || true'Sample output:
000
000NetworkPolicies are additive, so testing each branch before deleting the rule proves the AND behavior rather than merely describing it.
Remove the AND rule before demonstrating OR:
kubectl delete networkpolicy allow-and-rule -n backendSeparate from items: OR
Each list item is an alternative:
from:
- namespaceSelector:
matchLabels:
access: allowed
- podSelector:
matchLabels:
role: clientThis means: traffic from the labeled namespace, or from client Pods in the policy namespace (backend).
Apply and test the OR rule:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-or-rule
namespace: backend
spec:
podSelector:
matchLabels:
role: server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
access: allowed
- podSelector:
matchLabels:
role: client
ports:
- protocol: TCP
port: 8080kubectl apply -f allow-or-rule.yamlRetry until the frontend client succeeds:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okkubectl \
-n untrusted exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://backend.backend.svc.cluster.local.:8080 || true'Sample output:
000allowed=false
for attempt in {1..20}; do
result=$(
kubectl -n backend exec deploy/local-client -- sh -c 'curl -s --connect-timeout 2 http://backend:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "The OR podSelector branch did not become active." >&2
exit 1
fiSample output:
backend-ok| Client | AND | OR |
|---|---|---|
frontend/client |
Allowed | Allowed |
untrusted/client |
Blocked | Blocked |
backend/local-client |
Blocked | Allowed |
The AND rule would have blocked local-client unless its namespace also carried access=allowed. That single comparison is worth testing on your cluster when you teach the distinction.
After the OR comparison:
kubectl delete networkpolicy allow-or-rule -n backendOnly default-deny-ingress remains in backend before the egress examples.
Configure Egress Policies
Default-Deny Egress
Default-deny egress isolates outbound traffic for every Pod in a namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: frontend
spec:
podSelector: {}
policyTypes:
- EgressApply it to frontend:
kubectl apply -f default-deny-egress.yamlExisting inbound access to frontend Pods is unaffected — this policy does not declare ingress isolation. Outbound connections from frontend clients stop, including DNS lookups:
dns_blocked=false
for attempt in {1..20}; do
if kubectl -n frontend exec deploy/client -- nslookup kubernetes.default.svc.cluster.local. >/dev/null 2>&1; then
sleep 1
else
echo "DNS lookup blocked"
dns_blocked=true
break
fi
done
if [[ "$dns_blocked" != "true" ]]; then
echo "DNS remained available; verify egress policy enforcement." >&2
exit 1
fiSample output:
DNS lookup blockedReply traffic for connections allowed by the current policies is implicitly permitted. Whether a connection established before a policy change remains open is CNI-dependent, so verify changes with a new connection rather than reusing an existing session.
Restore DNS
Most workloads need DNS after egress isolation. Permit UDP and TCP port 53 to CoreDNS Pods in kube-system.
Confirm the DNS namespace and Pod labels on your cluster before publishing YAML:
kubectl get pods -n kube-system -l k8s-app=kube-dns --show-labelsAbridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:
NAME READY STATUS RESTARTS AGE LABELS
coredns-589f44dc88-7q8t5 1/1 Running 9 2d k8s-app=kube-dns,pod-template-hash=589f44dc88
coredns-589f44dc88-xcrkp 1/1 Running 0 13h k8s-app=kube-dns,pod-template-hash=589f44dc88Create the DNS egress allow policy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: frontend
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: 53kubectl apply -f allow-dns-egress.yamlSample output:
networkpolicy.networking.k8s.io/allow-dns-egress createdRetry until DNS resolution succeeds:
resolved=false
for attempt in {1..20}; do
if kubectl -n frontend exec deploy/client -- nslookup kubernetes.default.svc.cluster.local. >/dev/null 2>&1; then
resolved=true
break
fi
sleep 1
done
if [[ "$resolved" != "true" ]]; then
echo "DNS was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiDNS resolution works again:
kubectl -n frontend exec deploy/client -- nslookup kubernetes.default.svc.cluster.local.Abridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:
Name: kubernetes.default.svc.cluster.local
Address: 10.96.0.1Backend access remains blocked until you add an application egress rule. CNI implementations can differ in how they translate Service IPs versus Pod IPs; test against the DNS path your cluster actually uses. See Kubernetes DNS troubleshooting when lookups fail outside policy work.
Allow One Application Path
Permit frontend clients to reach only the backend namespace, backend server Pods, and TCP port 8080:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-egress
namespace: frontend
spec:
podSelector:
matchLabels:
role: client
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: backend
podSelector:
matchLabels:
role: server
ports:
- protocol: TCP
port: 8080kubectl apply -f allow-backend-egress.yamlSample output:
networkpolicy.networking.k8s.io/allow-backend-egress createdEgress alone is not enough. Restore backend ingress for the frontend namespace and client Pods:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-ingress
namespace: backend
spec:
podSelector:
matchLabels:
role: server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
access: allowed
podSelector:
matchLabels:
role: client
ports:
- protocol: TCP
port: 8080kubectl apply -f allow-frontend-ingress.yamlSample output:
networkpolicy.networking.k8s.io/allow-frontend-ingress createdKeep default-deny-ingress in backend so only listed sources reach the server.
Verify Bidirectional Permission
With both application policies now active, test the complete bidirectional path:
allowed=false
for attempt in {1..20}; do
result=$(
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
--connect-timeout 2 http://backend.backend.svc.cluster.local.:8080 || true'
)
if [[ "$result" == "backend-ok" ]]; then
printf '%s\n' "$result"
allowed=true
break
fi
sleep 1
done
if [[ "$allowed" != "true" ]]; then
echo "Traffic was not restored; verify NetworkPolicy enforcement." >&2
exit 1
fiSample output:
backend-okPublic egress remains blocked:
kubectl \
-n frontend exec deploy/client \
-- sh \
-c 'curl \
-s \
-o /dev/null \
-w "%{http_code}\n" \
--connect-timeout 5 http://1.1.1.1 || true'Sample output:
000Bidirectional policy is the production pattern: client egress plus server ingress must agree.
Allow External CIDRs with ipBlock
ipBlock matches IP ranges outside Pod and namespace identity selectors. Use it for external networks, not for identifying Pods — Pod IPs are ephemeral.
egress:
- to:
- ipBlock:
cidr: 192.0.2.0/24
except:
- 192.0.2.128/25
ports:
- protocol: TCP
port: 443except subtracts CIDR blocks from the allowed range. Source or destination address translation can occur before or after policy enforcement depending on the CNI and whether traffic passes through a Service IP. Keep external egress design brief, test the observed path, and avoid hard-coding Pod CIDRs as a stand-in for label selectors.
Inspect and Troubleshoot Policies
List Policies and Matching Labels
Kubernetes does not ship a single command that prints the fully combined effective policy for one Pod. Inspect objects and labels, then test traffic from controlled clients.
List policies cluster-wide:
kubectl get networkpolicy -AAbridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:
NAMESPACE NAME POD-SELECTOR AGE
backend allow-frontend-ingress role=server 18s
backend default-deny-ingress <none> 18s
frontend allow-backend-egress role=client 18s
frontend allow-dns-egress <none> 27s
frontend default-deny-egress <none> 41sDescribe one policy to read its human-readable summary:
kubectl describe networkpolicy allow-frontend-ingress -n backendAbridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:
Name: allow-frontend-ingress
Namespace: backend
Spec:
PodSelector: role=server
Allowing ingress traffic:
To Port: 8080/TCP
From:
NamespaceSelector: access=allowed
PodSelector: role=client
Not affecting egress traffic
Policy Types: IngressConfirm Pod and namespace labels match what your selectors expect:
kubectl get pods -n backend --show-labelskubectl get namespaces --show-labelsAbridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:
NAME STATUS LABELS
backend Active app=backend,kubernetes.io/metadata.name=backend
frontend Active access=allowed,kubernetes.io/metadata.name=frontend
untrusted Active kubernetes.io/metadata.name=untrustedControlled curl tests from known client Pods remain the practical verification step.
Common NetworkPolicy Mistakes
| Symptom | Likely cause | Fix |
|---|---|---|
| Policy has no effect | CNI does not enforce NetworkPolicy | Confirm enforcement with a deny test; install or enable a capable CNI |
| Rule never matches | Policy created in the wrong namespace | NetworkPolicy applies only within its own namespace |
| No Pods isolated | podSelector matches no Pod labels |
kubectl get pods --show-labels and align selectors |
| Namespace rule misses client | Namespace lacks the expected label | Label the namespace or use kubernetes.io/metadata.name |
| Unexpected allow/deny | AND versus OR indentation confused | Keep both selectors in one list item for AND; separate items for OR |
| Port rule blocks valid traffic | Policy port does not match Pod listen port | Test against the Pod port, not only the Service port |
| DNS fails after egress deny | No DNS egress allow rule | Permit UDP/TCP 53 to CoreDNS Pods |
| Client allowed, server blocks | Only ingress or only egress configured | Add matching rules on both sides |
| Service works in theory, not in practice | Backend Pod connectivity not verified first | Confirm direct backend reachability before blaming DNS or policy |
| Expect immediate deny without isolation | Policy does not select Pods or direction | Declare policyTypes and selectors that actually isolate the target |
| Later test succeeds unexpectedly | An earlier allow policy was never deleted | Policies combine additively; delete or replace old allow rules before the next comparison |
What's Next
- Audit Kubernetes Against the CIS Benchmark with kube-bench
- Kubernetes Ingress Rules with Host, Path and TLS Examples
- Protect Kubernetes Node Metadata and Kubelet Endpoints
References
- Network Policies — isolation, selectors, default policies, ports, ipBlock, and limitations
- Declare Network Policy — official practical task
- NetworkPolicy API — current networking.k8s.io/v1 field reference
- DNS for Services and Pods — Service DNS names used by the lab
- Network Plugins — CNI implementation context
- What is network policy? — Calico Kubernetes NetworkPolicy enforcement
Summary
NetworkPolicy turns selected Pods into allow-list endpoints for ingress, egress, or both. You built a three-namespace lab, recorded open connectivity, applied default-deny ingress in backend, and restored access one rule at a time with podSelector, namespaceSelector, port filters, and combined AND and OR selector shapes — deleting each demonstration policy before the next comparison so additive allows did not invalidate later tests.
The details that decide whether isolation actually works are easy to underestimate. Your CNI must enforce policies, rules must live in the correct namespace, and a flow needs matching permission on both sides when client and server Pods are isolated. Default-deny egress commonly breaks DNS until you explicitly allow UDP and TCP port 53 to your CoreDNS Pods — confirm those labels on your cluster rather than copying another distribution's YAML verbatim.
For verification, read kubectl describe networkpolicy beside real curl tests from labeled client Pods. When policy objects look correct but traffic still fails, validate Service endpoints and in-cluster DNS before rewriting selectors. Delete the policies when you finish the lab and confirm baseline connectivity returns — that reset is your proof the CNI is enforcing what you applied.

