Kubernetes NetworkPolicy with Examples

Isolate Kubernetes Pods with NetworkPolicy default-deny rules, allow ingress and egress with podSelector and namespaceSelector, permit DNS, and verify allowed and blocked paths between namespaces.

Published

Updated

Read time 20 min read

Reviewed byDeepak Prasad

Kubernetes NetworkPolicy isolating Pods with default deny ingress and egress rules
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 namespacesbackend, 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):

bash
kubectl get pods -n calico-system -l k8s-app=calico-node

Example output from the tested Calico cluster; Pod names, restart counts, and ages vary:

output
NAME                READY   STATUS    RESTARTS      AGE
calico-node-hc7ph   1/1     Running   1 (39h ago)   2d10h
calico-node-sc7p2   1/1     Running   0             24h

Running 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.
text
Non-isolated Pod  →  all traffic allowed

Ingress-isolated Pod  →  only listed ingress peers/ports allowed

Egress-isolated Pod  →  only listed egress peers/ports allowed

When 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.

yaml
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:

bash
kubectl apply -f networkpolicy-lab.yaml

Wait for every Deployment to become available:

bash
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=120s

Representative output:

output
deployment.apps/backend condition met
deployment.apps/local-client condition met
deployment.apps/client condition met
deployment.apps/client condition met

Test 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:

bash
kubectl -n frontend exec deploy/client -- curl -s --connect-timeout 5 http://backend.backend.svc.cluster.local.:8080

Sample output:

output
backend-ok

Repeat from the untrusted namespace:

bash
kubectl -n untrusted exec deploy/client -- curl -s --connect-timeout 5 http://backend.backend.svc.cluster.local.:8080

Sample output:

output
backend-ok

Test the in-namespace short name from local-client:

bash
kubectl -n backend exec deploy/local-client -- curl -s --connect-timeout 5 http://backend:8080

Sample output:

output
backend-ok

All 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.

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: backend
spec:
  podSelector: {}
  policyTypes:
  - Ingress

Apply it to backend:

bash
kubectl apply -f default-deny-ingress.yaml

An 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:

bash
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
fi

Sample output:

output
000

When 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:

bash
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'
bash
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:

output
000

Use 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:

yaml
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: 8080

Apply the allow policy while the default-deny policy remains in place:

bash
kubectl apply -f allow-client-ingress.yaml

Retry until the same-namespace client succeeds:

bash
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
fi

Sample output:

output
backend-ok

The frontend client in another namespace is still blocked because podSelector alone does not match cross-namespace Pods:

bash
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:

output
000

After completing the same-namespace podSelector test, remove the pod-only allow rule:

bash
kubectl delete networkpolicy allow-client-ingress -n backend

Allow a Selected Namespace

Permit ingress from namespaces labeled access: allowed:

yaml
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: 8080

namespaceSelector 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:

bash
kubectl apply -f allow-frontend-ns.yaml

Retry until the frontend client succeeds:

bash
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
fi

Sample output:

output
backend-ok

The untrusted namespace has no matching label, so its client remains blocked:

bash
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:

output
000

Restrict Ports and Protocols

ports under an ingress or egress rule filters destination ports on the selected Pods:

yaml
ports:
- protocol: TCP
  port: 8080

Omitting 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:

bash
kubectl patch networkpolicy allow-frontend-ns -n backend --type=json -p='[
    {
      "op": "replace",
      "path": "/spec/ingress/0/ports/0/port",
      "value": 80
    }
  ]'

Sample output:

output
networkpolicy.networking.k8s.io/allow-frontend-ns patched

Run the blocked curl test:

bash
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
fi

Sample output:

output
000

Restore the correct port:

bash
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:

bash
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
fi

Sample output:

output
backend-ok

A 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:

bash
kubectl delete networkpolicy allow-frontend-ns -n backend

Combine 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:

yaml
from:
- namespaceSelector:
    matchLabels:
      access: allowed
  podSelector:
    matchLabels:
      role: client

This means: Pods with role=client inside namespaces labeled access=allowed.

Apply and test the AND rule:

yaml
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: 8080
bash
kubectl apply -f allow-and-rule.yaml

Retry until the frontend client succeeds:

bash
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
fi

Sample output:

output
backend-ok
bash
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'
bash
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:

output
000
000

NetworkPolicies 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:

bash
kubectl delete networkpolicy allow-and-rule -n backend

Separate from items: OR

Each list item is an alternative:

yaml
from:
- namespaceSelector:
    matchLabels:
      access: allowed
- podSelector:
    matchLabels:
      role: client

This means: traffic from the labeled namespace, or from client Pods in the policy namespace (backend).

Apply and test the OR rule:

yaml
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: 8080
bash
kubectl apply -f allow-or-rule.yaml

Retry until the frontend client succeeds:

bash
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
fi

Sample output:

output
backend-ok
bash
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:

output
000
bash
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 "The OR podSelector branch did not become active." >&2
  exit 1
fi

Sample output:

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:

bash
kubectl delete networkpolicy allow-or-rule -n backend

Only 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:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: frontend
spec:
  podSelector: {}
  policyTypes:
  - Egress

Apply it to frontend:

bash
kubectl apply -f default-deny-egress.yaml

Existing inbound access to frontend Pods is unaffected — this policy does not declare ingress isolation. Outbound connections from frontend clients stop, including DNS lookups:

bash
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
fi

Sample output:

output
DNS lookup blocked

Reply 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:

bash
kubectl get pods -n kube-system -l k8s-app=kube-dns --show-labels

Abridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:

output
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=589f44dc88

Create the DNS egress allow policy:

yaml
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: 53
bash
kubectl apply -f allow-dns-egress.yaml

Sample output:

output
networkpolicy.networking.k8s.io/allow-dns-egress created

Retry until DNS resolution succeeds:

bash
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
fi

DNS resolution works again:

bash
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:

output
Name:	kubernetes.default.svc.cluster.local
Address: 10.96.0.1

Backend 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:

yaml
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: 8080
bash
kubectl apply -f allow-backend-egress.yaml

Sample output:

output
networkpolicy.networking.k8s.io/allow-backend-egress created

Egress alone is not enough. Restore backend ingress for the frontend namespace and client Pods:

yaml
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: 8080
bash
kubectl apply -f allow-frontend-ingress.yaml

Sample output:

output
networkpolicy.networking.k8s.io/allow-frontend-ingress created

Keep 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:

bash
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
fi

Sample output:

output
backend-ok

Public egress remains blocked:

bash
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:

output
000

Bidirectional 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.

yaml
egress:
- to:
  - ipBlock:
      cidr: 192.0.2.0/24
      except:
      - 192.0.2.128/25
    ports:
    - protocol: TCP
      port: 443

except 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:

bash
kubectl get networkpolicy -A

Abridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:

output
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>         41s

Describe one policy to read its human-readable summary:

bash
kubectl describe networkpolicy allow-frontend-ingress -n backend

Abridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:

output
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: Ingress

Confirm Pod and namespace labels match what your selectors expect:

bash
kubectl get pods -n backend --show-labels
bash
kubectl get namespaces --show-labels

Abridged example output; addresses, ages, generated Pod names, and unrelated cluster resources vary:

output
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=untrusted

Controlled 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


References


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.


Frequently Asked Questions

1. Do NetworkPolicies deny all traffic by default?

No. Pods are non-isolated until a NetworkPolicy selects them for ingress or egress isolation. Once selected, traffic in that direction is allowed only when a matching allow rule exists.

2. Why does my NetworkPolicy have no effect?

The most common cause is a CNI that does not enforce NetworkPolicy. The API accepts objects even when the dataplane ignores them. Confirm your network plugin supports policy enforcement before troubleshooting YAML.

3. Do I need both ingress and egress policies for a connection?

When both endpoints are isolated in the relevant direction, yes. The client Pod needs an egress allow rule and the server Pod needs a matching ingress allow rule. Either side missing a rule blocks the flow.

4. What is the difference between namespaceSelector and podSelector in one from entry?

When both appear under the same from or to list item, they combine with AND. Separate list items combine with OR. Indentation changes the meaning.

5. Why does DNS stop working after a default-deny egress policy?

Default-deny egress blocks outbound UDP and TCP to CoreDNS unless you add an explicit allow rule for DNS port 53 to the DNS Pods or Service endpoints in your cluster.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)