Troubleshooting¶
Port forward¶
Most commands below query the admin port. Open a port-forward in a separate terminal first:
kubectl -n coxswain-system port-forward svc/coxswain-shared-proxy-internal 8082:8082
/readyz returns 503 on startup¶
The readiness endpoint gates on every registered subsystem reaching Ready or Degraded. During startup it stays 503 until each subsystem reports at least one successful completion:
- Controller: all Kubernetes reflectors emit their first
InitDone(requires CRDs to be installed and controller RBAC to be correct), and the routing table is built at least once. - Proxy: the discovery client connects to the controller, bootstraps an SVID, and receives its first routing snapshot.
Inspect which subsystem is blocking:
curl -s http://localhost:8082/api/v1/health | jq .subsystems
Controller subsystem stuck in Pending (CRD missing or controller RBAC wrong):
{
"controller": {
"status": "Pending",
"checks": {
"httproute": "Pending",
"ingress": "Ready",
"gateway": "Pending",
"routing_table_built": "Pending"
}
}
}
Proxy subsystem stuck in Pending (discovery not yet connected or first snapshot not received):
{
"proxy": {
"status": "Pending",
"checks": {
"routing_table_loaded": "Pending"
}
}
}
Common causes for the controller being Pending:
- Gateway API CRDs not installed — install with kubectl apply -f .../standard-install.yaml
- Controller RBAC missing a permission — check kubectl -n coxswain-system logs -l app.kubernetes.io/name=coxswain for forbidden errors
Common causes for the proxy being Pending:
- Discovery endpoint unreachable — verify COXSWAIN_DISCOVERY_ENDPOINT points at the controller's discovery Service
- Trust bundle not yet published — kubectl -n coxswain-system get configmap coxswain-discovery-trust must exist; the controller publishes it at startup
- Bootstrap rejected — check for BootstrapRejected events: kubectl -n coxswain-system get events --field-selector reason=BootstrapRejected
- Wire-version mismatch — proxy logs FAILED_PRECONDITION and backs off permanently; see Wire-version skew
Routes are not being picked up¶
# Check the routing table — served from the controller's admin port, not the proxy's
curl -s http://<controller-admin-address>:8082/api/v1/fleet/proxies/<pod-name>/routes | jq .
# Check HTTPRoute status
kubectl describe httproute my-route
# Check Gateway status
kubectl describe gateway my-gateway
In the kubectl describe httproute output, look for a condition like this — ResolvedRefs: False means a backend Service cannot be found or a ReferenceGrant is missing for cross-namespace backends:
Status:
Parents:
Conditions:
Message: Backend "my-service" not found in namespace "default"
Reason: BackendNotFound
Status: False
Type: ResolvedRefs
And in the routes query above, the host entry will either be absent or show no upstream addresses.
TLS certificate is not being served¶
- Verify the Secret exists and has the correct type:
kubectl get secret my-tls -o jsonpath='{.type}'
# Should print: kubernetes.io/tls
-
Check Coxswain logs for
TLS Secret unusablemessages. -
Confirm the Secret is in the same namespace as the
IngressorGateway.
Leader election is not working¶
Check if the Lease exists and who holds it:
kubectl -n coxswain-system get lease
# NAME HOLDER AGE
# <name> coxswain-7d9f6b5c8-xk2pn 5m
If the HOLDER column is empty or the lease is expired, no replica has claimed leadership. Common causes:
- All replicas are crashing before they can acquire the lease — check
kubectl -n coxswain-system logs -l app.kubernetes.io/name=coxswain. - Clock skew between nodes — the default lease TTL (
--controller-lease-ttl=15s) assumes clocks are synchronised within a few seconds.
Dedicated proxy pod never becomes Ready¶
After the controller provisions a dedicated proxy (because a Gateway carries a parametersRef → CoxswainGatewayParameters), the pod should reach Running within the same time it takes any Deployment to pull its image and pass readiness checks.
If the pod is stuck:
# Check the Deployment events
kubectl -n <gateway-namespace> describe deployment <gateway-name>-coxswain
# Check the pod events
kubectl -n <gateway-namespace> describe pod -l gateway.networking.k8s.io/gateway-name=<gateway-name>
# Check controller logs for reconcile errors
kubectl -n coxswain-system logs -l app.kubernetes.io/component=controller --tail=100
Common causes:
Accepted=False, reason=InvalidParameterson the Gateway — theparametersRefpoints at aCoxswainGatewayParametersobject that doesn't exist or is in the wrong namespace. Create the object or fix the reference; the controller will reconcile and provision the pod.- Image pull error — the dedicated proxy uses the same image as the controller; verify
imagePullSecretsand registry credentials in the Gateway's target namespace. - Controller RBAC missing — the controller
ServiceAccountneeds permission to createDeployment,Service, andServiceAccountobjects in the Gateway's namespace. If the Helm chart was upgraded without runninghelm upgrade, re-run it to restore the latest ClusterRole.
Dedicated proxy stuck NotReady or Degraded¶
The dedicated proxy is a discovery client: it bootstraps an SVID from the controller and then opens a mTLS stream to receive its Gateway's routing snapshot. The proxy stays NotReady until the first snapshot arrives; it transitions to Degraded on any subsequent reconnect window.
# Check proxy logs for discovery errors
kubectl -n <gateway-namespace> logs deployment/<gateway-name>-coxswain | tail -50
# Check for bootstrap rejections (controller is the sole event emitter)
kubectl -n coxswain-system get events --field-selector reason=BootstrapRejected
Common causes:
- Discovery endpoint unreachable — the dedicated proxy's
COXSWAIN_DISCOVERY_ENDPOINTis rendered by the controller; verify the controller's discoveryServiceexists and the proxy pod can reach it. - SVID scope mismatch — the stream server logs
PERMISSION_DENIEDif the proxy's SVID does not match the expected ServiceAccount for the Gateway. Check that the SA name follows the GEP-1762 pattern ({gateway-name}-{gatewayclass-name}) and that the controller's registry entry is current. Reconciling the Gateway again (e.g. by adding/removing an annotation) forces a registry refresh. - Wire-version mismatch — proxy logs
FAILED_PRECONDITION; see Wire-version skew.
Provisioned resources not garbage-collected after Gateway deletion¶
When a Gateway is deleted, Kubernetes owner-reference GC removes the provisioned Deployment, Service, and ServiceAccount (all owner-referenced to the Gateway). The gateway.coxswain-labs.dev/dedicated-cleanup finalizer ensures the controller completes any remaining cleanup before Kubernetes finalizes the Gateway.
If resources are not disappearing after a kubectl delete gateway:
# Check whether the finalizer is still present (it should be removed by the controller)
kubectl get gateway <name> -n <ns> -o jsonpath='{.metadata.finalizers}'
# Check controller logs for cleanup errors
kubectl -n coxswain-system logs -l app.kubernetes.io/component=controller --tail=100 | grep dedicated-cleanup
Common cause: the controller is not running or has lost the leader lease — the finalizer is processed only by the active controller replica. If the controller is down or failing to elect a leader, the Gateway will be stuck in a terminating state until the controller recovers.
Controller stuck in Ingress-only mode¶
At startup, the controller probes for Gateway API CRDs (gatewayclass.gateway.networking.k8s.io, gateway.gateway.networking.k8s.io, httproute.gateway.networking.k8s.io). If any are absent, it drops the Gateway API reconciliation pipelines and runs as a pure Ingress controller.
Symptoms: no GatewayClass, Gateway, or HTTPRoute conditions are written; kubectl get gatewayclass coxswain returns nothing.
Fix: install the Gateway API CRDs and restart the controller.
# Install the standard-channel CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/latest/download/standard-install.yaml
# Restart the controller so it re-probes
kubectl -n coxswain-system rollout restart deployment/coxswain-controller
Ingress route is shadowed by a conflict¶
When two Ingress objects claim the same host and path combination, only one wins (see Multi-Ingress merging and conflict precedence). The losing Ingress's route is silently dropped from the routing table. The controller emits a Warning Event on the shadowed Ingress to make the conflict visible:
kubectl describe ingress <shadowed-ingress>
# ...
# Events:
# Type Reason Age From Message
# ---- ------ --- ---- -------
# Warning RouteConflict 1m coxswain Route on host app.example.com path /api is shadowed by default/winning-ingress
You can also query events directly:
kubectl get events --field-selector reason=RouteConflict -A
To resolve: ensure only one Ingress claims a given (host, path) slot, or migrate the conflicting rules into a single Ingress object.
Ingress annotation has no effect¶
If an ingress.coxswain-labs.dev/* annotation value is invalid, the controller ignores it (fail-open) and emits a Warning Event on the Ingress:
kubectl describe ingress <name>
# Events:
# Type Reason Age From Message
# ---- ------ --- ---- -------
# Warning InvalidAnnotation 1m coxswain read-timeout: invalid duration "5 seconds" — expected a Go duration string (e.g. "5s", "1m30s")
Query all annotation-parse warnings in the cluster:
kubectl get events --field-selector reason=InvalidAnnotation -A
On Kubernetes ≥ 1.30 with the Helm chart, the ValidatingAdmissionPolicy catches most invalid values at kubectl apply time — the admission rejection message matches the Event message format above.
High memory usage¶
The routing table is rebuilt from scratch on every reconcile. Very large clusters (thousands of HTTPRoute objects) may show elevated memory during rebuilds. Each completed rebuild releases the old table; the GC-free nature of Rust means this is deterministic rather than dependent on a garbage collector schedule.
Profile with:
curl -s http://localhost:8082/metrics | grep routing_table_rebuild_duration