Kubernetes NetworkPolicy: Default Deny Without Breaking DNS - 夜莺博客

Kubernetes NetworkPolicy: Default Deny Without Breaking DNS

By default a Kubernetes cluster is a flat network: any pod can reach any pod in any namespace, and a single compromised container can talk to your database, your metadata service and your control plane. NetworkPolicy is the fix, but it fails in two quiet ways — a CNI that silently ignores the policy, and a deny-all that nobody paired with a DNS exception. This guide covers the mental model, the patterns that cover most production cases, and how to verify enforcement is real.

The mental model, in three sentences

  1. Without a policy, everything is allowed.
  2. Once a policy selects a pod, everything not explicitly allowed is denied — policies are whitelists.
  3. Multiple policies on the same pod are additive; they never contradict, they union their allows.

Does your CNI actually enforce policies?

Plugin Behaviour
Flannel Ignores NetworkPolicy completely, with no warning.
kindnet Accept the YAML, never enforce it (default in kind clusters).
AWS VPC CNI Ignored until the Network Policy Controller is enabled (v1.14+).
Azure CNI Requires Azure NPM or Calico to be activated.
GKE default Enforcement must be chosen at cluster creation (Dataplane V2 / Calico).
Cilium, Calico, Antrea, Weave Enforce.
kubectl get pods -n kube-system -l k8s-app=calico-node
kubectl get pods -n kube-system -l k8s-app=cilium
kubectl describe daemonset aws-node -n kube-system | grep ENABLE_NETWORK_POLICY

A successful kubectl apply proves nothing. Verify the CNI, then test from a pod.

Pattern 1: default deny, both directions

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}          # every pod in the namespace
  policyTypes: [Ingress, Egress]

No ingress: or egress: block means no exceptions. Deploy this to every namespace before adding application rules — retrofitting deny-all after the fact means a long list of “why is this service broken” tickets.

Anatomy of a policy: the fields that matter

Field Meaning
podSelector Which pods in this namespace the policy applies to. An empty object {} selects every pod, and any matched pod becomes deny-by-default for the listed policyTypes.
policyTypes Which directions the policy controls. If omitted, Kubernetes infers it: Ingress is always implied, Egress only when an egress: block exists.
ingress / egress The whitelist rules. A missing block means no exceptions; an empty list means the same; each list item is one allowed source or destination.
namespaceSelector Matches namespaces by label, never by name — except that Kubernetes adds kubernetes.io/metadata.name automatically, which is why naming a namespace explicitly still works.

Two rules of thumb follow. A policy is not a firewall rule in the classic sense: it is a selector that flips a pod from "allow everything" to "allow only what is listed". And policies are additive, so the union of every policy matching a pod is what actually applies — there is no deny rule, and a later policy cannot take back something an earlier one allowed.

Pattern 2: the DNS rule nobody remembers

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
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

Deny-all egress without this rule breaks service discovery, which breaks almost everything, and the failure looks like an application bug rather than a network policy.

Pattern 3: allow one service to reach another

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
      namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: production
    ports:
    - protocol: TCP
      port: 8080

Read the from block carefully. It is a single list item containing both selectors, so this is an AND: pods labelled app=frontend that live in the namespace labelled kubernetes.io/metadata.name=production. Split those selectors into two list items and the rule silently becomes "any frontend pod anywhere, or any pod in production" — a much wider door than anyone intended.

Restrict the port as well. Omitting ports allows every port on the pod, which turns an application rule into an infrastructure-wide permit.

The AND-versus-OR trap

# AND: pods in a namespace labelled env=production
ingress:
- from:
  - podSelector: {matchLabels: {app: frontend}}
    namespaceSelector: {matchLabels: {env: production}}

# OR: any pod named frontend, or any pod in env=production
ingress:
- from:
  - podSelector: {matchLabels: {app: frontend}}
  - namespaceSelector: {matchLabels: {env: production}}

One list item with both selectors is an AND. Two list items are ORed. This single distinction is the source of most “my policy allows more than I intended” findings, especially when a namespace is not labelled at all — an empty match on a namespace selector behaves differently from what people expect.

Pattern 4: the three-zone namespace model

Most clusters reduce cleanly to three zones, and once the namespaces carry labels the policies write themselves.

  • Edge zone (ingress controllers, gateways). Accepts traffic from the load balancer, and reaches the application zone only on service ports.
  • Application zone (your workloads). Accepts traffic from the edge zone, its own namespace and the monitoring namespace; reaches the data zone only on database ports.
  • Data zone (databases, caches, queues). Accepts traffic only from the application zone on specific ports, and has the strictest egress, because this is where the interesting data lives.

Expressed as namespace labels, the model gives you a default that survives an audit and a rule set short enough for a human to review. It also makes the on-call question tractable: when something breaks after a policy change, there are only three places to look.

Pattern 5: controlled egress and the metadata endpoint

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: app
  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}
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32
    ports:
    - {protocol: TCP, port: 5432}
    - {protocol: TCP, port: 443}

The except clause on the metadata address belongs in every egress policy by default. Cloud instance credentials are served from 169.254.169.254, and a compromised pod that can reach it can often turn its workload identity into access to the whole account. Blocking that single address costs nothing and removes an entire class of attack.

Patterns worth having ready

  • Allow Prometheus scraping from the monitoring namespace on 9090/8080 — monitoring is almost always forgotten and then disabled “because it stopped working”.
  • Allow the ingress controller. External traffic through an Ingress still crosses the pod network, so the controller namespace must be allowed explicitly or all inbound traffic is dropped at the policy layer.
  • Block the cloud metadata endpoint (169.254.169.254) on egress to limit credential theft from a compromised pod.
  • Per-namespace policies, labels not names. Policies match labels; naming conventions that drift create silent holes.

Egress is where policies break things

Ingress rules break the traffic you can see coming in. Egress rules break things weeks later in ways that look like application bugs. The list to check before applying a default-deny egress policy:

  • DNS to kube-dns, UDP and TCP on port 53. Everything depends on it, and the symptom is a timeout rather than a DNS error.
  • The Kubernetes API for every controller, operator and sidecar that watches resources.
  • External dependencies: managed databases, object storage, SMTP relays, LDAP or Active Directory, NTP and any corporate proxy. These are forgotten most often because they sit outside the cluster and are invisible in a namespace diagram.
  • Metrics and logging destinations, when the agent pushes instead of being scraped.
  • The metadata endpoint, which should be blocked deliberately rather than allowed by omission.

Write that list down as an allow-policy first, then apply deny-all and the allow-policy in the same commit. There should never be a window where the namespace runs with deny-all and no exceptions.

Prove enforcement, do not assume it

A successful kubectl apply proves the API server accepted YAML. It proves nothing about whether packets are dropped. Test from inside the cluster with a pod that carries the tools.

kubectl run nettest --rm -it --image=nicolaka/netshoot --restart=Never -- bash
# from inside the test pod:
nc -zv backend.production.svc.cluster.local 8080    # denied: timeout or refused
dig +short kubernetes.default.svc.cluster.local     # answered only if DNS egress is allowed
curl -s --max-time 3 http://169.254.169.254/latest/meta-data/   # expect failure
nslookup backend.production.svc.cluster.local

Run the same tests from a namespace that should be allowed and one that should not. If both succeed, policy is not being enforced and you have a CNI problem, not a policy problem. If both fail, including the allowed case, you have probably forgotten the DNS rule or a namespace label. Testing from two sides is what separates "the YAML looks right" from "I know this works".

A rollout order that avoids a bad night

  1. Confirm the CNI actually enforces policy, using the checks at the top of this article. If it does not, stop — everything after this step is decoration.
  2. Label namespaces consistently, including the ones you consider unimportant. Unlabelled namespaces are where surprising allow rules hide.
  3. Apply default-deny-all together with the DNS allow rule, in a non-production namespace first.
  4. Add the ingress controller and monitoring rules.
  5. Add per-service rules one workload at a time, testing after each with nc -zv.
  6. Introduce egress policies last, starting with monitoring and batch jobs rather than user-facing paths.
  7. Record the expected reachability matrix next to the policies in the repository, so the next person can tell an intended block from a mistake.

Roll out in this order: deny-all plus DNS, then the ingress controller and monitoring, then per-service rules, testing with kubectl exec ... -- nc -zv between namespaces after each step.

Observability: knowing when a policy drops something

A deny rule with no visibility is indistinguishable from an outage. Before you rely on policies in production, make sure you can answer "which policy dropped this packet?" without guessing.

# Cilium
hubble observe --verdict DROPPED --follow
cilium monitor --type drop
# Calico
kubectl logs -n kube-system -l k8s-app=calico-node -c calico-node | grep -i denied
iptables-save | grep -i cali-fw

Whichever CNI you run, wire its drop events into the same alerting you use for the rest of the cluster. A policy drop should produce a signal an operator can see, not a mystery connection timeout three layers up in an application log.

Common mistakes worth naming

  • Applying deny-all without the DNS exception, then debugging "broken service discovery" for an hour.
  • Assuming a policy attached to a Service does something. Policies select pods, never Services or Ingress objects.
  • Forgetting pods running with hostNetwork: true, which many CNIs do not police at all.
  • Mixing AND and OR selector semantics inside one list.
  • Leaving policies in a namespace nobody owns. An unmanaged policy is a landmine with no author.

Frequently asked questions

Do policies cover traffic to and from the node itself? Not reliably. Traffic originating from the host network or from kubelet sits outside the pod network in most CNIs, which is why health checks can behave differently from pod-to-pod traffic.

Can I allow a specific IP instead of a pod? Yes, with ipBlock — but not for cluster pod IPs on most CNIs, where pod addresses are translated or virtualised and matching them with ipBlock is undefined behaviour. Use label selectors for in-cluster traffic.

Does a policy block ports nobody mentioned? Yes. Once a pod is selected by a policy with policyTypes: [Ingress], everything not explicitly allowed is dropped.

Do I need to restart pods after applying a policy? No. Policies take effect immediately at the network layer; there is nothing to restart.

Related reading: Calico, Cilium and Flannel compared, Kubernetes Gateway API versus Ingress, Docker networking drivers and nginx reverse proxy configuration.

原文链接:https://cheveo.de/en/blog/network-policies-cheatsheet