Kubernetes Security & Hardening›09 · Network policies & mTLS

Lesson 09 of 15 · Traffic Security

Network policies & mTLS

Turn Kubernetes' flat network into a least-privilege one: default-deny policies, allowing exactly the flows you need (including DNS), egress control, CNI-specific extensions, and encrypting service-to-service traffic with mTLS.

Advanced
Key wordsNetworkPolicydefault denyegressDNS egressnamespaceSelectorCalicoCiliummTLSSPIFFE

Flat by default

Out of the box, every pod can talk to every other pod in the cluster, in every namespace. One compromised pod can probe databases, admin UIs and metadata endpoints. NetworkPolicies turn this into deny by default, allow by intent.

A new school has no walls inside: anyone can wander into any room. NetworkPolicies build walls and add doors with rules: "the kitchen door only opens for the canteen staff", "the library only accepts visitors from classrooms". And don't forget the phone line (DNS). If you wall that off too, nobody can look up where anything is.

How policies behave

  • A pod with no policy selecting it: all traffic allowed.
  • Once any policy selects a pod for a direction (Ingress or Egress), only traffic explicitly allowed by some policy is permitted in that direction.
  • Policies are additive (allow-only), and enforced by the CNI: Calico and Cilium enforce them; some simple CNIs don't.

Step 1: default deny (ingress and egress)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: shop
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]

Step 2: allow DNS

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

Step 3: allow the real flows

web → api (port 8080), and ingress controller → web:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-from-web
  namespace: shop
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: web }
      ports:
        - { protocol: TCP, port: 8080 }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-egress-to-api
  namespace: shop
spec:
  podSelector:
    matchLabels: { app: web }
  policyTypes: ["Egress"]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: api }
      ports:
        - { protocol: TCP, port: 8080 }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-from-ingress
  namespace: shop
spec:
  podSelector:
    matchLabels: { app: web }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: traefik    # the ingress controller's namespace
      ports:
        - { protocol: TCP, port: 80 }

Selectors in one item vs separate items

Inside a single from item, namespaceSelector and podSelector together mean "these pods in those namespaces". As two separate list items (- namespaceSelector… and - podSelector…), they mean "either". One dash changes the meaning. Review policies carefully.

Beyond the standard API

Need Standard NetworkPolicy CNI extensions
Cluster-wide defaults ❌ (per namespace) Calico GlobalNetworkPolicy, Cilium CiliumClusterwideNetworkPolicy
Egress by DNS name (api.stripe.com) ❌ Cilium FQDN policies, Calico DNS policies
L7 rules (HTTP method/path) ❌ Cilium L7 policies
Policy logging ❌ Calico/Cilium flow logs (e.g. Hubble)

Kubernetes is also standardising cluster-wide AdminNetworkPolicy, supported by some CNIs.

mTLS: encrypt and authenticate workloads

NetworkPolicies filter by labels and IPs. mTLS adds encryption in transit and workload identity:

  • Service mesh (Istio, Linkerd): sidecars or node proxies establish mTLS automatically, with identities derived from ServiceAccounts. Policy can say "only shop/web may call shop/api", verified cryptographically.
  • CNI-level encryption (WireGuard/IPsec in Cilium or Calico) encrypts node-to-node traffic transparently, but doesn't give per-workload identity on its own.
  • SPIFFE/SPIRE issues workload identities that apps or proxies use for mTLS, across clusters and VMs.

See Service Mesh: Istio & Linkerd for the full treatment.

Try it: isolate the shop (kind + Calico)

  1. Create a kind cluster with networking.disableDefaultCNI: true and podSubnet: 192.168.0.0/16, then install Calico following its current quickstart manifest.
  2. Deploy web and api (any HTTP images, e.g. nginx:1.27 on port 80 for both; adjust ports in the policies).
  3. Before policies: confirm web → api and a test pod in default → api both work.
  4. Apply default-deny, then check DNS breaks (nslookup api fails). Add allow-dns and the flow policies.
  5. Verify: web → api works; default/test → api times out; api → internet times out.

Going deeper: policy at scale

  • Start in audit/observe mode where your CNI supports it (e.g. flow logs) to learn real traffic before denying.
  • Ship a default-deny + DNS baseline in every namespace's tenant kit (see Kubernetes Administration, lesson 29), and let teams add allow rules through GitOps.
  • Remember host-network pods and the node itself are often outside normal pod policies. Use host endpoint policies (Calico) or host firewall features (Cilium) for nodes.
  • Block the cloud metadata endpoint (169.254.169.254) for pods that don't need it. It's a classic credential-theft path.

Recap

  • Kubernetes networking is flat by default; the CNI enforces NetworkPolicies.
  • Pattern: default deny → allow DNS → allow each real flow (ingress and egress).
  • Careful with selector combinations; test with timeouts.
  • CNI extensions add global, FQDN and L7 rules; mTLS (mesh, SPIFFE) adds encryption and workload identity.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.