Defense in Depth#

No single network control stops every attack. Layer controls so that a failure in one does not compromise the system: host firewalls, Kubernetes network policies, service mesh encryption, API gateway authentication, and DNS security, each operating independently.

Host Firewall: iptables and nftables#

Every node should run a host firewall regardless of the orchestrator. Block everything by default:

# iptables: default deny with essential allows
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow SSH from management network only
iptables -A INPUT -p tcp --dport 22 -s 10.0.100.0/24 -j ACCEPT

# Allow kubelet API (for k8s nodes)
iptables -A INPUT -p tcp --dport 10250 -s 10.0.0.0/16 -j ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

The nftables equivalent is more readable for complex rulesets:

nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input tcp dport 22 ip saddr 10.0.100.0/24 accept
nft add rule inet filter input iif lo accept

On Kubernetes nodes, do not block inter-pod or CNI overlay traffic. The CNI plugin manages its own iptables rules. Host rules should focus on node-level services.

Kubernetes Network Policies#

By default, every pod can talk to every other pod. Network policies restrict this. They require a CNI that supports them (Calico, Cilium, Weave). Flannel alone does not enforce network policies.

Deny All by Default#

Start with a default-deny policy in each namespace, then allow specific traffic:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

This blocks all ingress and egress for every pod in the namespace. Nothing works until you add allow rules.

Allow Specific Traffic#

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress-to-database
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:  # Allow DNS resolution
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53

Critical: if you restrict egress, you must explicitly allow DNS (UDP 53 to kube-dns). Without it, service name resolution fails silently.

Namespace Isolation#

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            network-access: monitoring
        podSelector:
          matchLabels:
            app: prometheus

This allows only pods labeled app: prometheus from namespaces labeled network-access: monitoring to reach the target pods.

Service Mesh mTLS#

Network policies control which pods can connect but do not encrypt traffic or verify caller identity. A service mesh adds mutual TLS automatically.

Istio Strict mTLS#

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

All meshed traffic must be mTLS. Envoy handles certificate rotation and encryption transparently. Applications see plaintext on localhost.

Linkerd#

Linkerd enables mTLS by default with no configuration. Once the proxy is injected, all meshed traffic is encrypted. Verify with:

linkerd viz stat deploy -n production

The SECURED column shows the percentage of traffic using mTLS.

API Gateway Authentication#

The edge of your network needs authentication before requests reach internal services. Common patterns:

OAuth2/OIDC for user-facing APIs. The gateway validates JWT tokens against the identity provider. Envoy, Kong, and NGINX all support this.

API keys for machine-to-machine calls between external partners and your services. Rate-limit per key and rotate regularly.

mTLS at the gateway for high-security integrations. The client presents a certificate signed by your CA. This provides both encryption and client identity without passwords or tokens.

DNS Security#

DNS is often overlooked. An attacker who poisons DNS responses redirects traffic to malicious endpoints. DNSSEC signs responses so clients verify integrity. DNS-over-TLS encrypts queries to prevent eavesdropping. Within Kubernetes, ensure CoreDNS is not exposed outside the cluster and its RBAC is minimally scoped.

VPN and Bastion Hosts#

Cluster API servers and node SSH should never be public. WireGuard creates encrypted tunnels with minimal attack surface:

# On the bastion/gateway
wg set wg0 peer <client-public-key> allowed-ips 10.0.200.2/32

# On the client
wg set wg0 peer <server-public-key> endpoint bastion.example.com:51820 \
  allowed-ips 10.0.0.0/16

Bastion hosts are hardened jump boxes in a DMZ. Log all sessions. Use certificate-based SSH only.

Zero-Trust Principles for Kubernetes#

Zero trust means no implicit trust based on network location. Apply these principles: verify identity on every request (service mesh mTLS), authorize every action (RBAC and network policies), encrypt all traffic, log and audit everything, and assume breach (namespace isolation, least-privilege service accounts). Each layer above contributes. No single tool achieves zero trust alone.