Kubernetes networking
In this series (14 parts)
- Why Kubernetes exists
- Kubernetes architecture
- Core Kubernetes objects
- Kubernetes networking
- Storage in Kubernetes
- Kubernetes configuration and secrets
- Resource management and autoscaling
- Kubernetes workload types
- Kubernetes observability
- Kubernetes security
- Helm and package management
- GitOps with ArgoCD
- Kubernetes cluster operations
- Service mesh concepts
Kubernetes networking has three rules. Every pod gets its own IP. Pods can communicate with any other pod without NAT. Agents on a node can communicate with all pods on that node. Everything else builds on these guarantees.
The pod network model
Each pod receives a unique IP from the cluster’s pod CIDR range. Containers within a pod share that IP and communicate over localhost. This is fundamentally different from Docker’s default bridge networking, where containers get isolated IPs behind NAT.
The flat network means:
- Pod A on Node 1 can reach Pod B on Node 2 directly by IP.
- No port mapping is needed between pods.
- Network policies can filter traffic at the pod level.
A CNI (Container Network Interface) plugin implements this model. The plugin runs on each node and configures the network so pod-to-pod traffic flows correctly.
Popular CNI plugins:
| Plugin | Approach | Key feature |
|---|---|---|
| Calico | BGP or VXLAN | NetworkPolicy support, high performance |
| Cilium | eBPF | Kernel-level filtering, observability |
| Flannel | VXLAN overlay | Simple setup, no NetworkPolicy |
| Weave Net | Mesh overlay | Automatic encryption |
Services as stable endpoints
Pod IPs are ephemeral. When a pod dies and is replaced, it gets a new IP. Services solve this by providing a stable virtual IP (ClusterIP) that load-balances across matching pods.
When you create a Service, Kubernetes assigns it a ClusterIP from the service CIDR range. DNS resolves the service name to this IP. kube-proxy programs iptables or IPVS rules on every node so that traffic hitting the ClusterIP is forwarded to one of the backend pods.
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: production
spec:
type: ClusterIP
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 8080
- name: grpc
port: 9090
targetPort: 9090
DNS records created:
order-service.production.svc.cluster.localresolves to the ClusterIP.- Short form
order-serviceworks within the same namespace.
kube-proxy modes
kube-proxy translates Service definitions into network rules. It supports three modes.
iptables mode (default): Creates one iptables rule per service endpoint. For a service with 100 pods, that is 100 rules. Updates are O(n) where n is the total number of rules.
IPVS mode: Uses the Linux kernel’s IPVS (IP Virtual Server) for load balancing. IPVS is designed for this workload. It handles thousands of services efficiently with O(1) connection routing.
nftables mode: Available in Kubernetes 1.29+. Uses nftables instead of iptables for a cleaner rule set and better performance.
Switch to IPVS mode in kube-proxy configuration:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
scheduler: "rr"
Ingress and IngressController
Services handle internal routing. Ingress handles external HTTP/HTTPS traffic. An Ingress resource defines routing rules. An IngressController implements them.
graph LR Client["External Client"] --> LB["Cloud Load Balancer"] LB --> IC["Ingress Controller Pod"] IC --> |"Host: api.example.com"| SVC1["api-service"] IC --> |"Host: web.example.com"| SVC2["web-service"] SVC1 --> P1["Pod"] SVC1 --> P2["Pod"] SVC2 --> P3["Pod"] SVC2 --> P4["Pod"]
External traffic flows through the load balancer to the IngressController, which routes by hostname and path to backend Services.
A working Ingress manifest:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
- web.example.com
secretName: app-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
Common IngressControllers:
- NGINX Ingress Controller: Most widely used. Rich annotation set.
- Traefik: Auto-discovery, built-in Let’s Encrypt.
- HAProxy Ingress: High performance, enterprise features.
- AWS ALB Ingress Controller: Native AWS ALB integration.
The newer Gateway API is replacing Ingress with a more expressive model. It separates infrastructure concerns (GatewayClass, Gateway) from application routing (HTTPRoute).
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: production
spec:
parentRefs:
- name: main-gateway
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: api-service
port: 80
NetworkPolicy
By default, all pods can communicate with all other pods. NetworkPolicy restricts this. Think of it as a firewall at the pod level.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network-policy
namespace: production
spec:
podSelector:
matchLabels:
app: api-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
environment: production
podSelector:
matchLabels:
app: web-service
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
This policy allows the api-service pod to receive traffic only from web-service pods in production namespaces, and to send traffic only to the postgres pod and DNS. Everything else is blocked.
Important: NetworkPolicy requires a CNI plugin that supports it. Flannel does not. Calico and Cilium do.
A default-deny baseline
Start with denying all traffic, then open specific paths:
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. You then add targeted policies for each service.
Debugging networking issues
Common commands for troubleshooting:
# Check service endpoints
kubectl get endpoints order-service -n production
# Test DNS resolution from inside a pod
kubectl exec -it debug-pod -- nslookup order-service.production.svc.cluster.local
# Check if kube-proxy rules exist
kubectl exec -it debug-pod -- sh -c "iptables-save | grep order-service"
# Verify pod-to-pod connectivity
kubectl exec -it pod-a -- curl -s http://10.244.1.5:8080/healthz
# Check NetworkPolicy effects
kubectl describe networkpolicy api-network-policy -n production
When a service is unreachable, check this order: DNS resolution, endpoint list (are pods selected?), kube-proxy rules, NetworkPolicy, and finally pod health.
What comes next
Networking gets traffic to your pods. But stateful applications need persistent data. The next article covers storage in Kubernetes: PersistentVolumes, StorageClasses, and StatefulSets.