Why design, not just write
One policy is easy. A real app is a chain - the ingress controller, a frontend, a backend, a database, an outside partner API - spread over namespaces owned by different teams. Get one selector subtly wrong and either the site is down (504s everywhere) or, worse, everything works and the database is open to every pod in another namespace. This lesson is about reading and designing policies so neither happens.
What you need to know already: NetworkPolicy, isolation, peers, default deny and the DNS rule (16.29), the Ingress controller as a proxy in its own namespace (16.19), the 504 status (16.19), CIDR (8.3), YAML lists (a - starts a new list item).
The dash trap
In YAML a leading - starts a new list item. These two policies differ by one dash and mean very different things:
ingress:
- from:
- namespaceSelector: # ONE peer: pods labelled app=gateway
matchLabels: # IN namespaces labelled team=edge
team: edge
podSelector:
matchLabels:
app: gateway
ingress:
- from:
- namespaceSelector: # TWO peers (note the second dash):
matchLabels: # ANY pod in team=edge namespaces
team: edge # OR
- podSelector: # app=gateway pods in THIS namespace
matchLabels:
app: gateway
One dash = AND inside a peer. Two dashes = OR between peers. The second form passes review ("it mentions edge and gateway"), passes every functional test (the gateway can connect), and lets every pod of the edge namespaces in. Read policies with describe, which renders the structure explicitly:
Allowing ingress traffic:
To Port: 8080/TCP
From:
NamespaceSelector: team=edge
PodSelector: app=gateway
versus
From:
NamespaceSelector: team=edge
----------
PodSelector: app=gateway
The ---------- separator is the OR. When you review a policy, run describe and look for it.
Selecting namespaces
Custom namespace labels (team: edge) are only as trustworthy as whoever can label namespaces: anyone allowed to add team=edge to their namespace gets in. For "this exact namespace" use the automatic label, which holds the namespace's name and cannot be changed:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
namespaceSelector: {} = all namespaces (the whole cluster).
A three-tier baseline
A three-tier app is the classic shape: a frontend (web pages), a backend (business logic, an API) and a database, each only talking to its neighbour:
internet -> ingress-nginx -> frontend:8080 -> backend:8080 -> db:6379
backend -> 10.0.3.20:443 (partner API)
Policies in namespace shop, each small and named after its edge (one allowed connection between two parts). Shown compressed in YAML's one-line {...} form:
# 1. isolate everything
spec: {podSelector: {}, policyTypes: [Ingress, Egress]}
# 2. DNS for everyone (as in the previous lesson)
# 3. frontend accepts the ingress controller
spec:
podSelector: {matchLabels: {tier: frontend}}
ingress:
- from:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: ingress-nginx}}
podSelector: {matchLabels: {app.kubernetes.io/name: ingress-nginx}}
ports: [{port: 8080}]
# 4. frontend -> backend (egress on frontend, ingress on backend)
# 5. backend -> db (egress on backend, ingress on db)
# 6. backend -> the partner API outside the cluster
spec:
podSelector: {matchLabels: {tier: backend}}
policyTypes: [Egress]
egress:
- to:
- ipBlock: {cidr: 10.0.3.20/32}
ports: [{port: 443}]
Points that decide whether this works:
- The ingress controller is a client pod in another namespace. After the default deny, every Ingress for this namespace returns 504 (the controller's connection is dropped, not refused) until rule 3 exists.
- Traffic from the controller comes from the controller pod IP, not from the end user - you allow the controller, and restrict users at the Ingress (
whitelist-source-range) or the load balancer. - External egress is by IP (
ipBlock). Kubernetes NetworkPolicy has no rules by DNS name (an FQDN, "fully qualified domain name" like api.partner.com). Calico and Cilium add those in their own CRDs (16.26) (GlobalNetworkPolicy,CiliumNetworkPolicywithtoFQDNs). For a hosted outside API whose IPs keep changing, that - or an egress gateway, one fixed proxy all outbound traffic must pass - is the real answer. ipBlockfor pod IPs is a trap - pod IPs change. Select pods by label.
Why a policy with only ingress rules does not restrict egress
Isolation is per direction. policyTypes: [Ingress] isolates ingress only; the pod can still connect anywhere. A compromised frontend with an ingress-only policy can still send stolen data out to the internet, or probe every other service in the cluster. Baselines always isolate both directions, and allow egress explicitly.
The debugging checklist
1. which policies select the destination pod? k get netpol -n NS; describe each;
compare podSelector with pod labels
2. which select the source pod (egress)? same, in the source namespace
3. does a rule match the peer? namespace labels (k get ns --show-labels),
pod labels, AND vs OR
4. does it match the port? the POD port, protocol (UDP for DNS!)
5. is DNS allowed? nslookup from the source pod
6. does the CNI enforce policies at all? a connection that must fail - does it?
AdminNetworkPolicy / BaselineAdminNetworkPolicy (newer, cluster-scoped policies from the Kubernetes project, with real deny rules and priorities) exist for platform-wide guardrails; know the name, the everyday tool is still namespaced NetworkPolicy.
What you can now do:
- spot the one-dash / two-dash difference, in YAML and in
describe - design a default-deny baseline for a three-tier app behind an ingress controller
- walk the six-step checklist when "the policy blocks something it should not"