Kubernetes Networking — Architect-Level Deep Dive
1. Cluster Networking Model
Kubernetes networking rests on four foundational rules that every implementation must satisfy:
- Every Pod gets its own IP address — no NAT between pods, no port mapping gymnastics.
- Pods on any node can communicate with pods on any other node without NAT.
- Agents on a node (kubelet, system daemons) can communicate with all pods on that node.
- Containers within a Pod share network namespace — they see each other on
localhost, differentiated only by port.
This is the flat network model. Unlike Docker’s default bridge networking (which requires explicit port mapping and NAT), Kubernetes assumes a single flat L3 space where pod IPs are routable (or at least reachable via overlay) cluster-wide.
Pod networking mechanics:
- Each pod gets a network namespace, attached to a virtual ethernet pair (
veth), bridged to a node-local bridge (e.g.,cbr0orcni0). - The pause container (infra container) owns the network namespace; all other containers in the pod join it via
--net=container:<pause_id>. - Pod IPs come from a per-node CIDR block carved out of the cluster’s pod CIDR (e.g., cluster
10.244.0.0/16→ node gets10.244.1.0/24). The kube-controller-manager’snode-cidr-mask-sizeflag controls this allocation.
The architect’s mental model: think of three separate IP address spaces you must reconcile —
- Node network (the underlying VPC/subnet — your AWS VPC CIDR)
- Pod network (CNI-managed, e.g.,
10.244.0.0/16) - Service network (ClusterIP range, virtual, e.g.,
10.96.0.0/12)
In AWS EKS specifically, the default CNI (amazon-vpc-cni-k8s) breaks the “overlay” pattern — pod IPs come directly from VPC subnet ENI secondary IPs, meaning pods are first-class VPC citizens, directly routable, but you’ll hit IP exhaustion fast on smaller subnets (a classic interview/real-world gotcha — m5.large supports only 3 ENIs × 10 IPs). This is why prefix delegation mode (ENABLE_PREFIX_DELEGATION=true) matters at scale — it assigns /28 prefixes instead of individual IPs per ENI slot, massively increasing pod density per node.
2. CNI Plugins (Calico, Flannel, Cilium)
The Container Network Interface (CNI) is a spec, not an implementation — kubelet calls a CNI plugin binary at pod create/delete time, passing it network namespace info via stdin/env vars. The plugin’s job: allocate an IP, wire up the veth pair, and (optionally) program routing/policy.
Flannel — simplicity-first overlay network.
- Default backend: VXLAN (encapsulates pod traffic in UDP packets, decoupling pod network from underlying infra — works even when nodes aren’t on the same L2/L3 domain).
- No native NetworkPolicy support — Flannel is pure connectivity, no policy enforcement layer. You’d need to pair it with Calico (
Canal) for policy. - Lowest operational overhead, lowest feature ceiling. Good fit for dev/test clusters or when you genuinely don’t need micro-segmentation.
Calico — the “enterprise default” for years.
- Uses BGP for route distribution in non-overlay mode — each node becomes a BGP peer, advertising its pod CIDR; no encapsulation overhead, near bare-metal performance.
- Can fall back to IP-in-IP or VXLAN encapsulation when BGP isn’t viable (e.g., crossing L3 boundaries without BGP support, or in cloud VPCs that block arbitrary routing).
- NetworkPolicy is first-class and predates the upstream K8s NetworkPolicy API — Calico’s own
GlobalNetworkPolicyCRD gives you cluster-wide policies, not just namespace-scoped ones, plus layer 7 policy via Calico Enterprise/eBPF dataplane integration. - Enforcement happens via iptables (legacy) or eBPF dataplane (modern, faster, bypasses kube-proxy entirely if configured that way).
Cilium — the eBPF-native, architecturally distinct choice.
- Doesn’t rely on iptables for forwarding or policy at all (in eBPF mode) — programs are attached at the XDP/TC hook points in the kernel, giving line-rate performance and avoiding the iptables rule-explosion problem that plagues large clusters (thousands of Services × thousands of pods = iptables chains in the tens of thousands, with O(n) lookup cost).
- Identity-based security model: instead of IP-based policy matching, Cilium assigns each pod a security identity derived from labels, and policy decisions reference identities — this survives pod IP churn gracefully and scales better for policy evaluation.
- Hubble gives you flow-level observability (who talked to whom, was it allowed/denied, L7 visibility for HTTP/gRPC/Kafka) — this is the feature that tips most architects toward Cilium for production observability requirements.
- Can fully replace kube-proxy (
kubeProxyReplacement=true), handling Service load-balancing directly in eBPF — meaningfully better performance at scale (avoids iptables NAT chain traversal per packet). - Higher operational complexity, kernel version sensitivity (older kernels lack eBPF features you’d want), steepest learning curve of the three.
Architect’s decision framework:
| Need | Reach for |
|---|---|
| Dead simple, dev/test, no policy needs | Flannel |
| BGP-native, mature NetworkPolicy, hybrid on-prem/cloud | Calico |
| eBPF performance, L7 visibility, kube-proxy replacement, deep observability | Cilium |
| AWS EKS, VPC-native IP management | aws-vpc-cni (often paired with Calico/Cilium for policy only, since vpc-cni itself doesn’t enforce NetworkPolicy by default) |
3. Services: ClusterIP, NodePort, LoadBalancer
Services solve the problem that pod IPs are ephemeral — a Service gives you a stable virtual IP (VIP) and DNS name fronting a dynamic set of pod endpoints, tracked via Endpoints/EndpointSlice objects (label selector match).
ClusterIP (default):
- Virtual IP from the Service CIDR, only routable within the cluster. There’s no actual “listener” on that IP anywhere — it’s a routing fiction implemented by kube-proxy.
- kube-proxy historically implements this via iptables DNAT rules (each Service → chain of rules → random/weighted selection among endpoint pods) or, in newer setups, IPVS mode (better performance at high Service-count, uses kernel IPVS load balancer instead of linear iptables chain traversal — O(1) hash lookup vs O(n)).
- This is the layer that headless Services (
clusterIP: None) bypass — no VIP at all, DNS returns the actual pod IPs directly (critical for StatefulSets where clients need to address specific pod identities, e.g., Kafka brokers, Cassandra nodes).
NodePort:
- Superset of ClusterIP — additionally opens a port (default range
30000-32767) on every node’s IP, regardless of whether that node runs a matching pod. Traffic hitting<any-node-ip>:<nodeport>gets forwarded (possibly to a pod on a different node, which means an extra network hop unlessexternalTrafficPolicy: Localis set). externalTrafficPolicy: Localpreserves source IP and avoids the extra hop, but creates uneven load distribution if pod replicas aren’t evenly spread across nodes — a frequent real-world gotcha worth knowing for architect interviews.
LoadBalancer:
- Superset of NodePort — additionally provisions an external cloud load balancer (ELB/ALB/NLB in AWS) via the cloud-controller-manager, which then targets the NodePort across all nodes.
- In AWS specifically: Classic ELB (legacy, L4 only) vs NLB (
service.beta.kubernetes.io/aws-load-balancer-type: nlb, preserves source IP, handles millions of connections, L4) vs using ALB via Ingress instead (L7, path/host routing, but technically a different controller path — the AWS Load Balancer Controller intercepting Ingress objects, not the in-tree LoadBalancer Service type). - Architect note worth flagging in interviews: provisioning one LoadBalancer Service per microservice gets expensive and operationally messy at scale (one ELB/NLB per service = cost + IP sprawl). This is the core justification for Ingress as a Service-multiplexing layer.
4. Ingress & Ingress Controllers
Ingress is an API object describing L7 HTTP/HTTPS routing rules — host-based and path-based routing into one or more backend Services — but the Ingress object itself does nothing without an Ingress Controller actually watching and implementing it. This split (spec vs implementation) is a frequent point of confusion worth being crisp about.
Common controllers:
- NGINX Ingress Controller — the most widely deployed, runs as a pod (often DaemonSet or Deployment behind a single LoadBalancer Service), reads Ingress objects, generates nginx.conf dynamically, reloads.
- AWS Load Balancer Controller — translates Ingress objects into actual ALB resources (target groups, listener rules) — this is “Ingress” implemented as native AWS infrastructure rather than a pod-based proxy. Critical distinction for AWS-centric architecture: you’re provisioning real ALBs, not routing through an nginx pod.
- Traefik, HAProxy Ingress, Contour (Envoy-based), Istio Gateway — increasingly, Gateway API is superseding Ingress as the modern, more expressive spec (separates infrastructure/platform concerns from application routing concerns via
GatewayClass/Gateway/HTTPRoute— worth mentioning in any 2026-era architect conversation as the direction the ecosystem is moving).
TLS termination: Ingress supports a tls block referencing a Secret (cert + key) — termination typically happens at the controller/load balancer, with optional re-encryption to backend (TLS passthrough is controller-specific, e.g., NGINX ssl-passthrough annotation, or ALB’s TLS pass-through limitations that often push people toward NLB+cert-manager+app-level TLS for true end-to-end encryption requirements).
Annotations are the escape hatch — since the Ingress spec itself is minimal, controller-specific behavior (rewrite rules, rate limiting, canary weighting, sticky sessions, CORS) is configured almost entirely through annotations, which is also the spec’s biggest portability weakness — annotations are not portable across controllers. This is precisely the gap Gateway API is designed to close.
5. DNS in Kubernetes
CoreDNS (replaced kube-dns as default since 1.13) runs as a Deployment in kube-system, exposed via a ClusterIP Service (kube-dns by convention), referenced by every pod’s /etc/resolv.conf (injected by kubelet based on the cluster’s --cluster-dns flag).
Naming convention (the part everyone gets quizzed on):
<service-name>.<namespace>.svc.cluster.local
- Same-namespace pods can just use
<service-name>. - Cross-namespace requires at least
<service-name>.<namespace>. - For headless Services, DNS returns A/AAAA records for each pod (not a single VIP) — and for StatefulSet pods specifically, each gets its own stable DNS name:
<pod-name>.<service-name>.<namespace>.svc.cluster.local— this is the mechanism that makes ordered, addressable StatefulSet members possible (e.g.,kafka-0.kafka-headless.default.svc.cluster.local).
Pod DNS policy (dnsPolicy field) controls resolution behavior:
ClusterFirst(default) — queries go to CoreDNS first, with non-cluster-domain queries forwarded upstream.Default— inherits node’s resolv.conf (bypasses cluster DNS entirely — used for performance-sensitive workloads avoiding the CoreDNS hop).None+ customdnsConfig— full manual control, used when integrating with external DNS systems (Route53 private zones, on-prem DNS for hybrid clusters).
Operational gotcha worth flagging architecturally: CoreDNS becomes a real bottleneck/SPOF at scale if under-provisioned — classic symptom is intermittent DNS resolution failures under load, usually traced to CoreDNS pod CPU throttling or ndots:5 in resolv.conf causing unnecessary extra DNS queries (every non-FQDN lookup tries 5 search-domain permutations before succeeding) — a known performance trap for chatty microservices making many short-lived DNS lookups. NodeLocal DNSCache (a DaemonSet caching resolver on each node) is the standard mitigation.
6. Network Policies
NetworkPolicy is Kubernetes’ native (if minimal) micro-segmentation primitive — a namespaced resource that, by label selector, defines allowed ingress/egress traffic for a set of pods. Critically: with no NetworkPolicy applied, all traffic is allowed (default-allow). The moment any policy selects a pod, that pod’s unspecified traffic direction becomes default-deny for that direction.
Core building blocks:
podSelector— which pods this policy applies to.policyTypes—Ingress,Egress, or both.ingress[].from/egress[].to— allowed peers, expressed viapodSelector,namespaceSelector, oripBlock(CIDR-based, for external traffic).- Ports — protocol + port restriction per rule.
A foundational architect pattern — default-deny-all, then allow explicitly:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
Followed by targeted allow rules per service (e.g., “frontend can talk to backend on 8080,” “backend can talk to DB on 5432,” “only egress to DNS + specific external CIDR allowed”).
Critical limitation to call out: the upstream NetworkPolicy API is L3/L4 only — no HTTP method/path matching, no L7 awareness. If you need L7 policy (e.g., “allow GET but not DELETE on this path”), you need Calico’s L7 policy via Envoy sidecar, Cilium’s toFQDNs/toPorts.rules.http, or a service mesh (Istio AuthorizationPolicy). This is a very common “gap between what people assume K8s NetworkPolicy does and what it actually does” interview/design-review trap.
Another subtlety worth knowing: NetworkPolicy enforcement is entirely dependent on the CNI plugin implementing it — the API object can exist with zero effect if your CNI (e.g., plain Flannel, or aws-vpc-cni without an add-on) doesn’t enforce policy. This is precisely why production EKS clusters using vpc-cni for IP management often layer Calico for policy enforcement only (Calico for policy, vpc-cni for networking is a well-known combined deployment pattern), or move to Cilium for a single unified dataplane.