Routes and the four TLS modes, OVN-Kubernetes and the MTU fault everybody misdiagnoses, NetworkPolicy isolation that silently blinds monitoring, and the CSI chain where each stage fails in a different log.
Eight layers between the browser and the container, each able to drop the packet for its own reason.
The router is the only layer that returns a useful status code. Everything below it fails by dropping, which is why the triage table later works upwards from the pod rather than down from the client.
The diagram above is the high level: what the pieces are. These two are the ones you want when something is wrong — what is inside one of those boxes, and the path a request really takes through them.
Routes, the overlay, service endpoints and the policy model.
A Route is OpenShift's ingress object. It predates the Kubernetes
Ingress API, and OpenShift supports both — an Ingress is silently converted into
a Route by the ingress operator, which is worth knowing because the object you debug is the
Route even when the object you created was an Ingress.
What a Route gives you that plain Ingress does not: per-route TLS policy, weighted backends for canary splits, and configuration knobs (timeouts, balance algorithm, rate limits) as annotations that the router actually honours.
| Mode | Router does | Pod receives | Use when |
|---|---|---|---|
| edge | Terminates TLS | Plain HTTP | Default. Traffic inside the cluster is on the overlay. |
| passthrough | Forwards bytes, does not decrypt | TLS | The app must see the client cert (mTLS), or does its own TLS. |
| reencrypt | Terminates, then opens a new TLS connection | TLS | You need the router's cert externally and encryption internally. |
| none | Plain HTTP end to end | HTTP | Essentially never in production. |
passthrough cannot do path-based routing. The router never decrypts, so it
cannot read the path — it routes on SNI alone. A passthrough Route with a path:
set is a configuration that silently does not do what it says.
OVN-Kubernetes is the default CNI on modern OpenShift. Each node gets a slice of the cluster CIDR; pods get an address from their node's slice; traffic between nodes is encapsulated in Geneve over the node network.
Three consequences that matter operationally:
iptables -L
on the node tells you nothing about why a pod was blocked.A Service is a stable name and VIP; the real destination list is the
EndpointSlice. An endpoint appears there only when the pod is Ready.
So the single most common "the service returns 503" cause is not networking at all — it is a
readiness probe failing, which silently empties the slice.
Cluster DNS is CoreDNS, and names resolve as
<service>.<namespace>.svc.cluster.local. Short names work inside the
same namespace through the search path — which is also why a pod resolving
db in the wrong namespace gets a confusing answer rather than an error.
An empty cluster allows every pod to reach every other pod, in every namespace. NetworkPolicy is additive allow-listing with an unusual trigger: a pod is unrestricted until at least one policy selects it, and from that moment only traffic explicitly allowed by some policy reaches it.
That produces the classic self-inflicted outage. You add one policy allowing frontend → backend. The backend is now isolated, so its egress to the database — never mentioned in the policy — is unaffected (egress is a separate policyType) but the monitoring scrape from the openshift-monitoring namespace is now blocked, and your dashboards go blank without a single error.
Router, monitoring, and the DNS namespace all need explicit allowances once a namespace is isolated. The failure is silent in each case: a 503 from the router, a gap in Prometheus, and a DNS timeout that looks like a slow dependency.
Binding modes, access modes, the CSI chain, and the two settings you cannot change after creation.
A PVC that never binds is one of these arrows not completing. Which arrow tells you which log to open.
A StorageClass is the policy: which provisioner, what parameters, what happens
to the volume when the claim is deleted, and — critically — when the volume
gets created.
| volumeBindingMode | Volume is created | Failure mode |
|---|---|---|
Immediate | As soon as the PVC exists | Volume lands in AZ-a, pod is later scheduled to AZ-b, pod is Pending forever |
WaitForFirstConsumer | Once a pod using it is scheduled | PVC sits Pending until a pod appears — which looks broken but is correct |
On any cluster whose nodes span failure domains, WaitForFirstConsumer is the
right default and Immediate is a latent scheduling bug.
Two other fields decide whether you can recover later, and neither can be changed on an
existing volume: reclaimPolicy (Delete destroys the backend volume
when the PVC goes; Retain keeps it) and allowVolumeExpansion
(false means you can never grow it without a migration).
Access modes are a contract the driver enforces, not a lock Kubernetes applies:
ContainerCreating because it landed elsewhere.A Deployment with the default RollingUpdate strategy starts the new
pod before terminating the old one. With an RWO volume and the new pod on a different
node, the new pod cannot attach until the old one releases — and the old one will not terminate
until the new one is ready. It deadlocks until the progress deadline expires.
CSI splits the work across sidecars, and each one fails in its own log. Knowing which is which turns a storage incident from an afternoon into ten minutes:
| Stage | Component | Runs on | Symptom when it fails |
|---|---|---|---|
| Create volume | external-provisioner | Controller pod | PVC stays Pending, ProvisioningFailed event |
| Attach to node | external-attacher | Controller pod | Pod stuck ContainerCreating, FailedAttachVolume |
| Mount into pod | CSI node plugin | DaemonSet on the node | FailedMount, timeout after 2 minutes |
| Grow volume | external-resizer | Controller pod | PVC capacity never changes, no error on the PVC |
The pattern to internalise: provisioning and attaching are cluster-level, mounting is node-level. A FailedMount is a node problem — go to that node. A ProvisioningFailed is a backend problem — go to the controller and then to the storage system itself.
Expansion works if allowVolumeExpansion: true was on the
StorageClass at creation time. Edit the PVC's requested size and the resizer grows the backend
volume, then the filesystem — online, for most drivers. Some need a pod restart to finish the
filesystem step, which shows as capacity updated on the PV but not inside the container.
Shrinking is not supported. Not by Kubernetes, not by any CSI driver. The only route is create-new, copy, swap.
reclaimPolicy is the one that ends careers. With Delete, removing
a PVC destroys the backing volume immediately and irreversibly — including when the PVC is
removed as a side effect of deleting a namespace or a Helm release. With Retain,
the PV survives in Released state holding the data, and you can rebind it.
oc delete project deletes the PVCs in it. On a Delete-policy class that is an irreversible data loss with no confirmation prompt and no undo. For any class holding real data, set Retain — the cost is orphaned volumes you clean up deliberately, which is a much better problem.
Networking and storage failures both present as "the pod is not working", and both have a layered path where the failing layer is rarely the one the symptom points at. Walk the path.
| Test | Command | If this works, the fault is above |
|---|---|---|
| Is the Route admitted? | oc get route app -o yaml | grep -A4 conditions | DNS or the load balancer |
| Does the router know about it? | oc -n openshift-ingress rsh deploy/router-default cat haproxy.config | grep app | The Route object |
| Does the Service have endpoints? | oc get endpointslice -l kubernetes.io/service-name=app | The router |
| Is the pod Ready? | oc get pod -l app=app | The Service selector |
| Does the pod answer directly? | oc rsh deploy/app curl -s localhost:8080/health | The readiness probe |
| Does another pod reach it? | oc run t --rm -it --image=curlimages/curl -- curl app.ns.svc:8080 | NetworkPolicy or DNS |
| Event on the pod | Layer | Go to |
|---|---|---|
WaitForFirstConsumer | Normal — not a failure | The pod's scheduling, not the PVC |
ProvisioningFailed | Backend refused to create it | external-provisioner logs, then the storage system |
FailedAttachVolume / Multi-Attach | Still attached elsewhere | oc get volumeattachment; likely an RWO rollout |
FailedMount after ~2 min | Node-level mount | CSI node DaemonSet on that node |
| Mounted but read-only | SCC / fsGroup / filesystem | The pod's securityContext and the image's ownership |
If the report is 'some requests work, big ones hang', 'it works from inside the cluster but not from outside', or 'TLS connects then stalls' — check MTU before anything else. It costs one ping -M do and it is the answer far more often than its reputation suggests.
| Command | What it answers |
|---|---|
oc get route -A | Every route and the host it claims |
oc get route r -o yaml | grep -A5 conditions | Whether the router admitted it — a 503's real cause |
oc -n openshift-ingress get pods -o wide | Where the routers run |
oc -n openshift-ingress rsh deploy/router-default cat haproxy.config | What the router actually configured |
oc get endpointslice -l kubernetes.io/service-name=<s> | Whether a Service has any Ready backend |
oc get networkpolicy -A | Which namespaces are isolated |
oc get network.operator cluster -o yaml | CNI, cluster CIDR and MTU |
oc rsh <pod> ping -M do -s 1372 <ip> | The real path MTU |
oc get pvc -A --field-selector=status.phase=Pending | Every stuck claim in the cluster |
oc get sc | Binding mode, reclaim policy, expansion — before you commit |
oc get volumeattachment | Whether a volume is attached and to which node |
oc describe pvc <c> | The provisioner's own error message |
oc get pv | grep Released | Retained volumes waiting to be rebound |
oc adm must-gather -- /usr/bin/gather_network_logs | The supported network dump |