Introduction
Ingress routes external HTTP(S) traffic to Services inside your cluster, but the Ingress resource alone does not expose anything. A working request travels through multiple layers that each must be healthy. Most Kubernetes Ingress errors are not caused by a single YAML field; they are request-path failures across DNS, load balancers, controllers, Services, endpoints, and application Pods.
Troubleshoot by testing every hop in order, not by randomly editing manifests.
Client
|
DNS
|
External IP / Load Balancer
|
Ingress Controller Service
|
Ingress Controller Pod
|
Ingress Rule
|
Application Service
|
EndpointSlice
|
Application Pod
What you will learn:
- How to verify each layer with concrete commands.
- How to diagnose Kubernetes Ingress 404/502/503/504 and TLS errors.
- How to separate the Ingress API from the controller implementation.
Primary concept: an Ingress is configuration consumed by an Ingress controller. Creating an Ingress resource does not install or configure a controller.
Build a minimal working example
Save the following as example.yaml. It creates a namespace, a simple HTTP echo Deployment, a ClusterIP Service, and an Ingress with host-based routing at path "/" using pathType: Prefix. Adjust <namespace> and <host>.
apiVersion: v1
kind: Namespace
metadata:
name: <namespace>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo
namespace: <namespace>
spec:
replicas: 2
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: hashicorp/http-echo:0.2.3
args: ["-text=ok"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: echo-svc
namespace: <namespace>
spec:
selector:
app: echo
ports:
- name: http
port: 80
targetPort: 5678
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echo
namespace: <namespace>
spec:
ingressClassName: nginx
rules:
- host: <host>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo-svc
port:
number: 80
Apply and inspect:
kubectl apply -f example.yaml
kubectl get pods,svc,ingress -n <namespace>
kubectl describe ingress echo -n <namespace>
Healthy signs:
- Pods Ready=1/1 and STATUS=Running.
- Service has a stable ClusterIP and a port 80 -> 5678 mapping.
- Ingress shows an ADDRESS (external IP or hostname) after your controller provisions it.
Start with the request path
Use this quick table to test each layer deliberately.
| Layer | Typical symptom | Diagnostic command | Success looks like | Likely fix |
|---|---|---|---|---|
| DNS | Host does not resolve | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">dig +short <host></code> | Returns IP or LB hostname | Create/adjust DNS A/AAAA/CNAME |
| External LB | Connection refused/timeout | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -v http://<host>/</code> | TCP connect succeeds | Provision LB, open firewall/SG, wait for provisioning |
| Controller Service | Empty Ingress ADDRESS | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get svc -A</code> | Controller svc has EXTERNAL-IP or NodePort | Configure Service type, fix cloud/LB setup |
| Controller Pods | 503/timeout | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get pods -A</code> | Controller Pods Running/Ready | Install/repair controller |
| IngressClass | Ingress ignored | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get ingressclass</code> | Matching class exists and is default or named in spec | Set <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">spec.ingressClassName</code> correctly |
| Ingress resource | 404 default backend | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl describe ingress</code> | Accepted, rules listed, no errors | Fix host/path, annotations, class |
| Backend Service | 502/503 | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get svc <svc> -o yaml</code> | Port/selector correct | Correct name/port/selector |
| EndpointSlice | 503/504 | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get endpointslice -l kubernetes.io/service-name=<svc> -n <ns></code> | Addresses with Ready=true | Fix selector, Pod readiness |
| Application Pods | 502/504 | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl describe pod <pod></code> | Ready, listening on 0.0.0.0 | Fix probes, bind address, container args |
| TLS | Browser warnings | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">openssl s_client -connect <host>:443 -servername <host></code> | Certificate matches host, not expired | Fix Secret, SANs, chain |
| App response | App 404 | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -v -H "Host: <host>" http://<ingress-ip>/</code> | App headers/body as expected | Fix app routes or rewrites |
Error 1: No Ingress controller is installed
An Ingress object is only configuration. Without a controller, nothing listens.
kubectl get pods -A | grep -E "ingress|traefik|haproxy|alb|gateway"
kubectl get deployments -A | grep -E "ingress|traefik|haproxy|alb|gateway"
kubectl get ingressclass
Look for ingress-nginx, Traefik, HAProxy, or your cloud vendor controller. Installation differs across bare metal, minikube/kind, managed Kubernetes, and VMs. Use your platform’s documented method.
Error 2: IngressClass is missing or incorrect
The controller handling your Ingress is chosen by spec.ingressClassName or the cluster’s default IngressClass.
kubectl get ingressclass
kubectl describe ingressclass <class-name>
kubectl get ingress echo -n <namespace> -o yaml
Example spec:
spec:
ingressClassName: nginx
Use the class name your controller advertises; names are implementation-specific.
Error 3: Ingress has no external address
kubectl get ingress -A shows an empty ADDRESS when:
- No controller handles the resource.
- The controller Service has no EXTERNAL-IP (
<pending>). - Load balancer is still provisioning or not supported on your infra.
- Bare-metal LoadBalancer integration (e.g., MetalLB) is missing/misconfigured.
- The controller is exposed via NodePort instead.
Check:
kubectl get svc -A
kubectl describe svc <controller-service> -n <controller-namespace>
kubectl get events -A --sort-by=.metadata.creationTimestamp
If EXTERNAL-IP is <pending>, solve the Service or infrastructure issue first.
Error 4: DNS does not point to the Ingress endpoint
Validate DNS and host-based routing:
dig +short <host>
nslookup <host>
curl -v http://<host>/
To bypass DNS while preserving the Host header:
curl -v --resolve <host>:80:<ingress-ip> http://<host>/
curl -vk --resolve <host>:443:<ingress-ip> https://<host>/
--resolve proves the Ingress and backend work even if public DNS is wrong.
Error 5: Default backend returns 404
A controller-generated 404 typically means host or path did not match any rule, or the wrong controller handled the request.
curl -v -H "Host: <host>" http://<ingress-ip>/
kubectl describe ingress <name> -n <namespace>
kubectl get ingress <name> -n <namespace> -o yaml
Differentiate:
- Ingress 404: headers often include controller-specific identifiers (e.g., nginx). Fix host/path, class, or annotations.
- Application 404: request reached your app; adjust app routing.
Error 6: Path matching and rewrites
pathType: Prefixmatches/apiand/api/v1.pathType: Exactmatches the path literally.ImplementationSpecificis controller-defined and not portable.
Standard example without rewrite:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo-svc
port:
number: 80
Ingress-nginx-specific rewrite example (controller-specific, not portable):
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
Do not copy nginx annotations to Traefik, HAProxy, or cloud controllers.
Error 7: Wrong Service name or port
The Ingress backend must reference the Service port, not the Pod’s containerPort.
backend:
service:
name: api-service
port:
number: 8080
Check mappings and names:
kubectl get svc <service-name> -n <namespace> -o yaml
kubectl describe svc <service-name> -n <namespace>
kubectl get deployment <deployment-name> -n <namespace> -o yaml
Service.port maps to Service.targetPort, which maps to the containerPort (or a named port) inside the Pod.
Error 8: Service has no endpoints
A Service can exist with zero ready endpoints. Confirm:
kubectl get endpoints <service-name> -n <namespace>
kubectl get endpointslice -n <namespace> -l kubernetes.io/service-name=<service-name>
kubectl describe svc <service-name> -n <namespace>
kubectl get pods -n <namespace> --show-labels
Common causes:
- Selector does not match Pod labels.
- Pods are not Ready (failing probes).
- Pods are terminating or in another namespace.
Fix selectors to match labels; Ingress cannot reference Services in other namespaces via the standard backend.
Error 9: Pods running but not Ready
Running does not mean routable. Inspect probes and logs:
kubectl get pods -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace>
Example readiness probe:
readinessProbe:
httpGet:
path: /healthz
port: 5678
initialDelaySeconds: 2
periodSeconds: 5
Ensure the app listens on 0.0.0.0, not 127.0.0.1.
Error 10–12: 502, 503, 504
- 502 Bad Gateway: often wrong targetPort, backend refusing connections, HTTP vs HTTPS mismatch.
- 503 Service Unavailable: no ready endpoints, backend Service missing, or controller reload errors.
- 504 Gateway Timeout: slow app, dependency latency, NetworkPolicy blocking, or controller timeout settings.
Test the Service from inside the cluster to isolate Ingress vs backend:
kubectl run curl-test --rm -it --restart=Never \
--image=curlimages/curl -n <namespace> -- \
curl -v http://<service-name>:<port>/
Success here but failure through Ingress points to controller, routing rules, or protocol/TLS configuration.
TLS errors: Secrets and certificates
TLS requires a Secret of type kubernetes.io/tls in the same namespace as the Ingress. Hosts must match the certificate SANs.
spec:
tls:
- hosts:
- app.example.com
secretName: app-example-tls
Validate:
kubectl get secret app-example-tls -n <namespace>
kubectl describe ingress <name> -n <namespace>
openssl s_client -connect <host>:443 -servername <host> | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Check for expiration, chain completeness, and correct hostname.
Works with port-forward but not through Ingress
kubectl port-forward deployment/<deployment> 8080:<container-port> proves the Pod listens and responds. kubectl port-forward service/<service> 8080:<service-port> proves the Service routes to endpoints. If both work but the public route fails, focus on controller, DNS, TLS, or external networking.
Controller logs and generated configuration
Identify controller Pods generically:
kubectl get pods -A -l app.kubernetes.io/component=controller
Then inspect logs in the correct namespace. Example (ingress-nginx only):
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller
Look for admission errors, config reloads, and upstream health messages.
A disciplined troubleshooting workflow
- Confirm DNS:
dig +short <host>. - Confirm external reachability:
curl -v http(s)://<host>/. - Controller installed and Ready:
kubectl get deployments,pods -A. - Correct IngressClass:
kubectl get ingressclassand Ingressspec.ingressClassName. - Inspect Ingress and events:
kubectl describe ingress. - Test host/path:
curl -v -H "Host: <host>" http://<ingress-ip>/. - Verify Service name and port:
kubectl describe svc. - EndpointSlices Ready:
kubectl get endpointslice. - Test inside cluster:
kubectl run ... curl http://<svc>:<port>/. - Check controller logs.
- Check application logs and probes.
- Validate TLS Secret and certificate hostname.
- Review NetworkPolicies/firewalls.
- Re-test externally.
Practical end-to-end incident
Symptom (example output):
curl -v https://app.example.com/returns 503.
Evidence:
kubectl get ingress echo -n demo
# ADDRESS set; rules OK
kubectl get svc echo-svc -n demo
# Port 80 -> 5678
kubectl get endpointslice -n demo -l kubernetes.io/service-name=echo-svc
# No endpoints listed
kubectl get deploy echo -n demo -o yaml | grep labels -A2
# Deployment pods labeled app=echo-app (mismatch!)
Root cause: Service selector app=echo does not match Pod label app=echo-app, so there are zero ready endpoints.
Fix (align labels):
spec:
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
Validate:
kubectl apply -f deployment.yaml
kubectl get endpointslice -n demo -l kubernetes.io/service-name=echo-svc
# Endpoints now present
curl -v https://app.example.com/
# 200 OK
Prevention: alert on Services with no ready endpoints; lint selectors in CI; test routes after deploy.
Quick decision table
| Symptom | Most likely layer | First command | Common cause | Next action |
|---|---|---|---|---|
| DNS failure | DNS | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">dig +short <host></code> | Missing/old record | Fix DNS A/AAAA/CNAME |
| Connection refused | LB/Service | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -v http://<host>/</code> | LB not ready, firewall | Verify LB, open ports |
| Timeout | LB/Controller/App | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -v</code> + controller logs | NetworkPolicy, slow app | Test Service in-cluster |
| Controller 404 | Ingress rule | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl describe ingress</code> | Host/path mismatch | Correct rule/pathType |
| Application 404 | App | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -v</code> headers | App route missing | Fix app routes |
| 502 | Backend | In-cluster curl to Service | Wrong targetPort/protocol | Align ports/protocols |
| 503 | Endpoints | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get endpointslice</code> | No ready endpoints | Fix selector/probes |
| 504 | App/deps | App logs/metrics | Slow backend | Optimize or adjust timeouts |
| TLS warning | TLS | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">openssl s_client ...</code> | Name/expiry/chain | Fix Secret/cert |
| Wrong certificate | TLS/SNI | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">openssl s_client -servername</code> | Host mismatch | Provide per-host cert |
| Redirect loop | Proxy/app | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl -IL</code> | Proto/header issues | Trust X-Forwarded-Proto, fix redirects |
| Empty external address | Infra | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get svc -A</code> | Pending LB | Configure LB/NodePort |
| Works by IP only | Ingress rule | <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">curl --resolve</code> | Host mismatch | Correct host rules |
Ingress versus Gateway API
Ingress remains widely used. Gateway API offers richer routing and role separation, but still requires a compatible controller and the same disciplined troubleshooting across DNS, Services, EndpointSlices, Pods, and TLS. Migrating does not eliminate these fundamentals.
Final troubleshooting checklist
- Controller installed and Pods Ready
- Controller Service reachable (EXTERNAL-IP/NodePort)
- Correct
spec.ingressClassName - DNS resolves to the Ingress endpoint
- Host and path rules correct, proper
pathType - Backend Service name and port correct
- Service selector matches Pod labels
- EndpointSlices contain Ready addresses
- Application listens on 0.0.0.0
- TLS Secret exists in same namespace; cert matches hostname and is valid
- NetworkPolicy/firewalls allow controller <-> Pods and external <-> controller
- Controller and application logs inspected
Conclusion
Most Kubernetes Ingress errors are not YAML typos; they are broken links in the end-to-end request path. Start at DNS and the external endpoint, move through the controller and Ingress rules, and finish at the Service, EndpointSlices, and Pods. Distinguish controller 404s from application 404s, and validate TLS without guessing. Reliable fixes come from isolating the failing layer, proving each hop with commands, and only then changing configuration.