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 ACCEPTThe 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 acceptOn 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
- EgressThis 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: 53Critical: 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: prometheusThis 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: STRICTAll 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 productionThe 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/16Bastion 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.