Skip to content

NetworkPolicy in practice

The four pre-approved rules are all ingress rules: intra-namespace pod traffic, intra-tenant pod traffic, Traefik ingress, and Prometheus scraping. No platform rule touches egress — outbound traffic (including DNS) works out of the box only because egress is unrestricted until a policy with policyTypes: Egress selects the pod. You need a custom NetworkPolicy when:

  • A pod must accept traffic from a source outside your tenant (rare — requires an RCS ticket for the cross-tenant side, plus your own allow rule for the destination).
  • You want to restrict a pod’s outbound traffic to specific destinations — writing an egress policy is how you lock down the otherwise-unrestricted outbound side.

On the ingress side the floor is default-deny, so every custom ingress rule is additive — it opens a lane without weakening the existing posture. On the egress side the first policy that selects a pod flips the default: outbound traffic is then limited to what your policies allow.

A NetworkPolicy lives in the namespace where the destination pod runs. It selects the destination pod(s) via podSelector and then declares which sources (ingress) or destinations (egress) are allowed.

Key fields:

  • podSelector — selects which pods in this namespace the rule applies to. An empty selector {} means “all pods in this namespace”.
  • ingress — who can send traffic to the selected pods.
  • egress — where the selected pods can send traffic to.
  • policyTypes — must list Ingress, Egress, or both. If you only list Ingress, the rule does not affect egress at all (and vice versa).

Example: allow ingress from a specific pod in the same namespace

Section titled “Example: allow ingress from a specific pod in the same namespace”

This rule lets pods labelled app: backend receive traffic on port 8080 from pods labelled app: frontend, both in the same namespace.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: <your-tenant>-prod
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

This rule lets pods labelled app: data-loader reach an external API at 203.0.113.0/24 on port 443. Egress is unrestricted until a policy with policyTypes: Egress selects a pod — this policy is that first restriction for the pods it selects, so it must also allow DNS explicitly or name resolution breaks.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-external-api
namespace: <your-tenant>-prod
spec:
podSelector:
matchLabels:
app: data-loader
policyTypes:
- Egress
egress:
# Allow DNS (required when specifying explicit egress rules)
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow the external API
- to:
- ipBlock:
cidr: 203.0.113.0/24
ports:
- protocol: TCP
port: 443

Cross-namespace traffic within the same tenant

Section titled “Cross-namespace traffic within the same tenant”

The pre-approved intra-tenant rule already permits cross-namespace traffic within your tenant. If you need to restrict it further — for example, allowing only a specific pod in namespace <your-tenant>-staging to reach a pod in <your-tenant>-prod — use namespaceSelector alongside podSelector:

ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <your-tenant>-staging
podSelector:
matchLabels:
app: deployer

Note the indentation: namespaceSelector and podSelector under the same - element means both must match (AND logic). If they are separate - elements, either one matching is enough (OR logic). Getting this wrong is the most common source of overly permissive rules.

When traffic is silently dropped, the problem is almost always a NetworkPolicy that does not match the way you think it does.

Step 1 — Verify the policy exists and selects the right pods:

Terminal window
kubectl get networkpolicy -n <your-tenant>-prod
kubectl describe networkpolicy <policy-name> -n <your-tenant>-prod

Check the PodSelector field in the describe output. <none> means an empty podSelector, which selects every pod in the namespace — describe shows the spec, not the match result. To test what a selector actually matches, run kubectl get pods -n <your-tenant>-prod -l <selector>.

Step 2 — Verify pod labels match the selector:

Terminal window
kubectl get pods -n <your-tenant>-prod --show-labels

Compare the labels on the source and destination pods against what the policy expects. Label typos (e.g. app: front-end vs app: frontend) are the single most common cause of “my policy doesn’t work.”

Step 3 — Check that policyTypes includes the direction you care about:

A policy with policyTypes: [Ingress] does not affect egress at all. If traffic is being dropped on the outbound side, you need Egress in policyTypes and an explicit egress allow.

Step 4 — Test connectivity:

Terminal window
kubectl exec -n <your-tenant>-prod <source-pod> -- curl -v --connect-timeout 5 http://<dest-service>:8080/

A connection timeout with no response means the packet was dropped by NetworkPolicy. A connection refused means the packet reached the destination but nothing is listening — that is a Service or application issue, not a NetworkPolicy issue.

MistakeSymptomFix
Egress rule without DNS allowPods lose all name resolutionAdd explicit DNS egress to kube-dns
AND vs OR selector indentationRule is wider or narrower than intendedPut namespaceSelector and podSelector under the same list element for AND
Policy in the wrong namespaceTraffic still blockedPolicy must be in the namespace of the destination pod
Label typo in selectorSelector matches zero podsCompare kubectl get pods --show-labels against the policy