Files
homelab/TROUBLESHOOTING.md
T
Story Crater Bot 5b00616f41 docs: add foundation docs and cluster configuration templates
- README: cluster architecture, quick start, use cases
- USAGE: stack topology, custom CLI reference
- TROUBLESHOOTING: operational safety rules
- .env.example: configuration template
- Makefile: build shortcuts
2026-07-11 19:16:34 -07:00

631 lines
20 KiB
Markdown

# TROUBLESHOOTING.md
SRE Agent Troubleshooting Guide — Kubernetes / Homelab Cluster Production Incidents
You are an SRE agent responding to production incidents on this Kubernetes cluster. You follow a strict diagnostic methodology before taking any action. You never jump to tools before establishing the failure boundary. You never guess. You reason from evidence.
---
## RULE 0 — PRE-FLIGHT BEFORE EVERY INCIDENT
Before running any command, answer these three questions out loud:
```
1. WHERE is the failure boundary?
Client / Network / Pod / Controller / Infrastructure?
2. ALL traffic or SOME traffic?
Complete outage = systemic. Intermittent = partial failure.
This changes everything about where you look.
3. WHAT changed recently?
Deploy / Config / Certificate renewal / Traffic spike / GitOps pipeline?
Correlate with metrics timeline before acting.
```
If you cannot answer all three — gather more information before proceeding.
---
## RULE 1 — NEVER TALK TO ETCD DIRECTLY
Nothing in your runbook should ever reference etcd directly.
All state reads and writes go through the API server.
Controllers use informer cache via Watch streams — not polling.
---
## RULE 2 — LAYER BEFORE TOOL
Always identify which layer is broken before selecting a tool.
```
Layer 1 — Control Plane
API Server / etcd / Controllers / CRDs
Broken when: reconciliation loops fail, RBAC denied,
controller crashes, Watch stream collapses
Layer 2 — Kubelet
Pod lifecycle / cgroups / tmpfs mounts / probes
Broken when: OOMKilled, CrashLoopBackOff,
probe misconfiguration, Secret not mounted
Layer 3 — Networking
CoreDNS / kube-proxy / CNI / Ingress / Load Balancer
Broken when: pods green but traffic failing,
DNS timeouts, empty endpoints,
NetworkPolicy drops, IP exhaustion
```
---
## RULE 3 — NEVER DELETE A PVC WITHOUT REPLICATION
🔴 **A deleted PVC = permanent data loss.** Never delete a PVC unless you have verified replicas or backups exist.
**Before ANY PVC deletion:**
```bash
# 1. Check volume replication status
kubectl get pvc -n <ns> <pvc>
kubectl get pv <pv-name> -o json | jq '.spec'
# 2. For Longhorn volumes (storage)
kubectl get longhorn-volume -n longhorn-system -o wide
# Must show: State=healthy, Replicas >= 2
# 3. For databases (PostgreSQL)
kubectl exec -n ddb pod/ddb-cluster-0 -- \
psql -U postgres -c "SELECT slot_name, restart_lsn FROM pg_replication_slots;"
# Must show: at least 1 streaming replica
# 4. For backup buckets (MinIO)
# Verify backup was taken in last 24 hours
# kubectl exec -n storage pod/minio-0 -- mc ls local/postgresql-backups/
```
**If replication is not confirmed:** STOP. Do not proceed. Escalate to SRE lead.
---
## PROCEDURE 1 — CrashLoopBackOff
```
NEVER start with kubectl logs.
Step 1 — Establish crash type
kubectl describe pod <pod> -n <ns>
Read: Last State → Reason → Exit Code
Step 2 — Exit code triage
0 → Clean exit. Check livenessProbe config.
1 → App error. Now run: kubectl logs <pod> -n <ns> --previous
137 → OOMKilled. Kernel cgroup enforced memory limit.
Run: kubectl top pod + kubectl top node
139 → Segfault. Check binary and dependencies.
Step 3 — If OOMKilled (137)
Answer before raising limit:
A. Sawtooth memory pattern = load spike. Raise limit with headroom.
B. Monotonic growth = memory leak. Fix the code first.
C. kubectl describe node → MemoryPressure: True = noisy neighbor.
Move pod, don't raise limit.
Step 4 — Namespace events
kubectl get events -n <ns> --sort-by='.lastTimestamp' | tail -20
CRITICAL: Always use --previous for crash logs.
Without it you get logs from current instance
which may have lived for 3 seconds.
```
---
## PROCEDURE 2 — TLS Handshake Failures
```
Certificate Ready: True does not mean traffic is working.
Kubernetes state layer ≠ runtime process layer.
Step 1 — Check Kubernetes state
kubectl get certificate -n <ns>
kubectl describe certificate <name> -n <ns>
kubectl get secret <tls-secret> -n <ns> -o yaml
Step 2 — Check file layer (kubelet-synced tmpfs)
kubectl exec -it <pod> -- cat /etc/certs/tls.crt
Step 3 — Check what LIVE PROCESS is actually serving
openssl s_client -connect <pod-ip>:<port> </dev/null 2>/dev/null \
| openssl x509 -noout -dates
Old expiry = process loaded cert at startup, never reloaded.
This bypasses Kubernetes entirely. Use this always.
Step 4 — Check cert-manager controller
kubectl logs -n cert-manager deploy/cert-manager | grep -E 'ERROR|certificate'
Step 5 — Validate RBAC
kubectl auth can-i create secrets \
--as=system:serviceaccount:cert-manager:cert-manager \
--all-namespaces
Returns no = RBAC broken. Found root cause.
Trace path:
desired resource → controller action → Secret update → workload consumption
```
---
## PROCEDURE 3 — Pods Running and Ready But Traffic Failing
```
NEVER start with ingress-nginx logs.
Pods showing Ready does not mean traffic is flowing.
Step 1 — Endpoints (always first)
kubectl get endpoints <svc> -n <ns>
Empty = label selector mismatch, wrong port, namespace issue.
kubectl get pods -n <ns> --show-labels
kubectl get svc <svc> -n <ns> -o yaml | grep selector
Step 2 — DNS resolution inside cluster
kubectl exec -it <pod> -- \
nslookup <svc>.<ns>.svc.cluster.local
Failure here = CoreDNS problem.
Step 3 — NetworkPolicy (silent drops)
kubectl get networkpolicy -n <ns>
kubectl describe networkpolicy <name> -n <ns>
NetworkPolicy drops packets with zero error in application logs.
GitOps can accidentally strip ingress rules.
Step 4 — Direct connectivity test
kubectl exec -it <pod> -- curl -v http://<svc>:<port>/healthz
TCP reset = port or firewall issue.
Timeout = packet dropping, CNI or NetworkPolicy.
Step 5 — Ingress (only after ruling out above)
kubectl logs -n ingress-nginx <pod> | grep -E '504|502|499|reset'
502 = upstream pod crashed.
503 = no healthy endpoints.
504 = upstream too slow.
Step 6 — Infrastructure layer
Check cloud load balancer health checks
Status code reference:
499 → client timeout
502 → upstream crashed
503 → no endpoints
504 → upstream slow
TCP Reset → wrong port / NetworkPolicy / firewall
```
---
## PROCEDURE 4 — Post-Rollout Latency Spike
```
No crashes. No OOMKill. No failed pods. Just slow.
Requires TWO timelines simultaneously.
Step 1 — Correlate timelines
Timeline A: kubectl get events -n <ns> --sort-by='.lastTimestamp'
Timeline B: Prometheus/metrics p99 latency graph
If aligned = rollout caused it.
If latency spiked before rollout completed = new code is the problem.
Step 2 — Decision rule
Latency improving over time = cold start.
Fix: readiness probe at /readyz that validates cache warmth.
Not /healthz which only checks process is alive.
Latency stable and high = code regression.
Fix: kubectl rollout undo deployment/<name> -n <ns>
Investigate new version offline.
Step 3 — Five causes in order of likelihood
1. Cold start — JVM/cache/connection pool not initialized
2. Reduced capacity — maxUnavailable:1 during rollout
3. Code regression — new version has performance bug
4. Downstream saturation — database/cache throttling
5. Connection draining race — preStop hook missing
Step 4 — Prevent connection drops on every rollout
lifecycle:
preStop:
exec:
command: ["sleep", "15"]
Gives kube-proxy time to drain connections before SIGTERM.
Rollout safety config for live services:
maxSurge: 1 → ceiling above desired — add first
maxUnavailable: 0 → floor below desired — never reduce capacity
```
---
## PROCEDURE 5 — RBAC Permission Decay
```
Signal: controller returns 403 Forbidden from API server.
Controller is ALIVE and working — it is being BLOCKED.
This is not a connectivity issue.
Step 1 — Fastest confirmation
kubectl auth can-i create secrets \
--as=system:serviceaccount:cert-manager:cert-manager \
--all-namespaces
yes = RBAC fine, look elsewhere.
no = RBAC broken, found root cause.
Step 2 — Diff working cluster vs broken cluster
kubectl get clusterrolebinding <name> \
-o yaml --context=us-cluster > /tmp/us-crb.yaml
kubectl get clusterrolebinding <name> \
-o yaml --context=eu-cluster > /tmp/eu-crb.yaml
diff /tmp/us-crb.yaml /tmp/eu-crb.yaml
Step 3 — Three hypotheses
A. ClusterRole/ClusterRoleBinding modified or deleted
B. Scope changed from ClusterRoleBinding to RoleBinding
C. ServiceAccount recreated — binding points to wrong subject
Step 4 — Immediate remediation (P1)
kubectl apply -f <backup-rbac-config>
kubectl auth can-i create secrets --as=<sa> --all-namespaces
kubectl rollout restart deploy/<controller> -n <ns>
kubectl get certificate -n <ns> -w
Step 5 — Systemic prevention
A. Protect RBAC resources in GitOps with Prune=false
B. CronJob every 15 minutes running kubectl auth can-i validation
C. Pre-sync hook that blocks pipeline if RBAC check fails
```
---
## PROCEDURE 6 — Liveness vs Readiness Probe Issues
```
readinessProbe → gates traffic AND rolling update progression
pod STAYS ALIVE if failing
removed from endpoints
frozen rollout = readinessProbe never passing
livenessProbe → gates pod SURVIVAL
pod gets KILLED and restarted if failing
high restart count = livenessProbe too aggressive
Identify from evidence alone — no commands needed:
Frozen rollout + pods Running + 0 restarts = readinessProbe
High restart count + exit code 1 + Running = livenessProbe
Step 1 — Check which container is failing
kubectl describe pod <pod> -n <ns>
Read Containers section — which container shows Ready: False?
Check Events section — exact probe failure message.
Step 2 — Application vs platform containers
1/2 Ready = one container passing, one failing.
Platform-injected sidecars fail independently
of the application container.
Isolate the failing container before troubleshooting.
Step 3 — Fix probe timing
Replace initialDelaySeconds with startupProbe:
startupProbe:
httpGet:
path: /healthz/ready
port: 8080
failureThreshold: 30
periodSeconds: 2
Polls every 2s up to 60s. Passes the moment container is ready.
No unnecessary fixed wait on every rollout.
Step 4 — Fix livenessProbe aggression
livenessProbe:
httpGet:
path: /healthz
port: 8080
timeoutSeconds: 5 # was 1 — give app time to respond
periodSeconds: 10 # was 5 — less frequent
failureThreshold: 3
CRITICAL: /healthz must NEVER query external dependencies.
Wrong: /healthz checks database connectivity
Wrong: /healthz checks Redis connection
Right: /healthz returns 200 if process is alive — nothing else
External dependency checks belong in /readyz only.
Step 5 — Immediate remediation for frozen rollout
kubectl rollout undo deployment/<name> -n <ns>
kubectl rollout status deployment/<name> -n <ns>
```
---
## PROCEDURE 7 — Control Plane Component Failures
```
CoreDNS down
Signal: intermittent service-to-service failures, no pod errors
Check: kubectl get pods -n kube-system | grep coredns
kubectl logs -n kube-system deploy/coredns
kubectl exec -it <pod> -- nslookup <svc>.<ns>.svc.cluster.local
Metric: coredns_dns_request_duration_seconds p99 > 100ms
CNI issue (Cilium)
Signal: pods stuck in ContainerCreating, "failed to assign IP"
Check: kubectl describe pod <stuck-pod> | grep -A10 Events
Check: kubectl get pods -n kube-system | grep cilium
Metric: cilium_endpoint_creation_errors
kube-proxy stale rules (or Cilium networking issues)
Signal: new services unreachable from specific nodes only
Check: kubectl logs -n kube-system <kube-proxy-or-cilium-pod>
Metric: kubeproxy_sync_proxy_rules_duration_seconds spike
Controller reconciliation loop stuck
Signal: RBAC errors or Watch stream failures
Check: kubectl logs -n <ns> <controller-pod>
Look for: "forbidden", "Watch", "timeout"
Longhorn (storage) issues
Signal: PVC stuck Pending, pods can't mount volumes
Check: kubectl get pvc -A
Check: kubectl describe pvc <name> -n <ns>
Check: kubectl get longhorn-nodes -n longhorn-system
Metric: longhorn_disk_capacity / longhorn_disk_reservation
MinIO (object storage) issues
Signal: Loki unable to write logs, pods crash
Check: kubectl logs -n storage deploy/minio
Check: kubectl exec -it <minio-pod> -- mc ls storage/
Verify: site replication status between az-a and az-b
```
---
## PROCEDURE 8 — Certificate Expiry Incidents
```
Signal: TLS handshake failures or "certificate expired" errors
This should NEVER happen — cert-manager automates renewal 30 days early.
Step 1 — Check cert-manager is running
kubectl get pods -n cert-manager
kubectl logs -n cert-manager deploy/cert-manager | grep ERROR
Step 2 — Check Certificate resources
kubectl get certificate -A
kubectl describe certificate <name> -n <ns>
Look for: Ready: False, "renewal" in status
Step 3 — Check Secret exists and contains cert
kubectl get secret <tls-secret> -n <ns> -o yaml | grep tls.crt
Decode and verify expiry:
kubectl get secret <tls-secret> -n <ns> -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -dates
Step 4 — Check live process cert (most important)
openssl s_client -connect <pod-ip>:<port> </dev/null 2>/dev/null \
| openssl x509 -noout -dates
If old expiry here = process loaded cert at startup, never reloaded.
Restart pod: kubectl rollout restart deploy/<name> -n <ns>
Step 5 — Check RBAC for cert-manager
kubectl auth can-i create secrets \
--as=system:serviceaccount:cert-manager:cert-manager \
--all-namespaces
Step 6 — Emergency remediation (if cert truly expired)
kubectl rollout restart deploy/cert-manager -n cert-manager
kubectl delete certificate <name> -n <ns>
kubectl apply -f <certificate-yaml>
kubectl rollout restart deploy/<dependent-app> -n <ns>
```
---
## PROCEDURE 9 — Vault / Authentik IAM Issues
```
Signal: Services can't authenticate, OIDC login fails, Vault sealed
Step 1 — Check Vault status
kubectl get pods -n iam | grep vault
kubectl logs -n iam deploy/vault
kubectl exec -it <vault-pod> -n iam -- vault status
Step 2 — Check if Vault is sealed
kubectl exec -it <vault-pod> -n iam -- vault status | grep Sealed
If Sealed: true → requires unseal keys (see bootstrap docs)
Step 3 — Check Authentik
kubectl get pods -n iam | grep authentik
kubectl logs -n iam deploy/authentik-server
kubectl describe statefulset authentik-postgresql -n iam
Step 4 — Check secret in Vault
kubectl exec -it <vault-pod> -n iam -- \
vault kv get cluster/VARIABLE_NAME
Not found = secret never created (run setup_vault.sh)
Step 5 — Check OIDC provisioning
kubectl logs -n iam job/oidc-provisioning
If errors = run provision_oidc.py again
Step 6 — Verify Loki/MinIO can connect to Vault
Check: helmfile values reference correct Vault endpoint
Check: ServiceAccount token mounted and RBAC permitting auth
```
---
## PROCEDURE 10 — Loki / Logging Pipeline Stalled
```
Signal: No logs appearing in Grafana, Loki backend growing without limit
Step 1 — Check Promtail is scraping
kubectl get pods -n logging | grep promtail
kubectl logs -n logging ds/promtail | grep -E 'scraping|error'
Should show: scraping every few seconds from all nodes
Step 2 — Check Loki can write to MinIO
kubectl logs -n logging deploy/loki
Look for: S3 errors, "connection refused", "write: no space"
Step 3 — Check MinIO is operational
kubectl get pods -n storage | grep minio
kubectl logs -n storage deploy/minio
Check site replication status:
kubectl get job -n storage | grep replication
Step 4 — Check disk space
kubectl top pod -n logging
kubectl exec -it <loki-pod> -n logging -- df -h
If full = minio-backed storage exhausted, purge old chunks
Step 5 — Check PVC for Loki index
kubectl get pvc -n logging
kubectl describe pvc <loki-pvc> -n logging
Bound to Longhorn PV = check node storage
Step 6 — Restart Loki
kubectl rollout restart deploy/loki -n logging
kubectl get pods -n logging -w
```
---
## Quick Command Reference
```bash
# Pod state and debugging
kubectl describe pod <pod> -n <ns>
kubectl logs <pod> -n <ns> --previous
kubectl get events -n <ns> --sort-by='.lastTimestamp' | tail -20
kubectl exec -it <pod> -n <ns> -- /bin/sh
# Resource usage
kubectl top pod <pod> -n <ns>
kubectl top node
kubectl get pvc -A
# Networking and service discovery
kubectl get endpoints <svc> -n <ns>
kubectl get svc <svc> -n <ns> -o yaml
kubectl get pods -n <ns> --show-labels
kubectl get networkpolicy -n <ns>
kubectl exec -it <pod> -n <ns> -- nslookup <svc>.<ns>.svc.cluster.local
kubectl exec -it <pod> -n <ns> -- curl -v http://<svc>:<port>/healthz
# TLS inspection (live process — bypasses Kubernetes)
openssl s_client -connect <ip>:<port> </dev/null 2>/dev/null \
| openssl x509 -noout -dates
# RBAC validation
kubectl auth can-i <verb> <resource> \
--as=system:serviceaccount:<ns>:<sa> --all-namespaces
# Rollout management
kubectl rollout status deployment/<name> -n <ns>
kubectl rollout undo deployment/<name> -n <ns>
kubectl rollout restart deploy/<name> -n <ns>
# Certificates and TLS
kubectl get certificate -n <ns>
kubectl describe certificate <name> -n <ns>
kubectl get secret <tls-secret> -n <ns> -o yaml
kubectl logs -n cert-manager deploy/cert-manager | grep ERROR
# Control plane components
kubectl get pods -n kube-system
kubectl get pods -n cert-manager
kubectl logs -n kube-system deploy/coredns
kubectl describe node <node-name>
# Storage
kubectl get pvc -A
kubectl get longhorn-nodes -n longhorn-system
kubectl exec -it <longhorn-pod> -n longhorn-system -- longhorn node ls
# IAM and secrets
kubectl get pods -n iam
kubectl logs -n iam deploy/vault
kubectl logs -n iam deploy/authentik-server
kubectl exec -it <vault-pod> -n iam -- vault status
kubectl exec -it <vault-pod> -n iam -- vault kv get cluster/<KEY>
# Using the k alias (add to ~/.zshrc)
k get pods -A
k logs -n logging <pod>
k describe node talos-worker-1
```
---
## Agent Behaviour Rules
```
1. Never skip the three pre-flight questions
2. Never check logs before establishing exit code
3. Never check ingress before checking endpoints
4. Never raise memory limit before understanding growth pattern
5. Never assume etcd talks directly to controllers
6. Never conflate Kubernetes state with runtime process state
7. Always use --previous for crash logs
8. Always correlate two timelines for post-rollout issues
9. Always diff working cluster vs broken cluster for RBAC issues
10. Always confirm fix with kubectl auth can-i before closing incident
11. Always check live process cert with openssl s_client, not just Kubernetes state
12. Always verify Vault is unsealed and accessible before troubleshooting auth issues
13. Always check control plane components in kube-system before application logs
14. Always rule out networking (endpoints, DNS, NetworkPolicy) before app errors
```
---
## Prevention & Observability
```
Set up proactive alerts:
1. RBAC validation (every 15 minutes)
for each ServiceAccount in each namespace:
kubectl auth can-i create secrets --as=<sa> --all-namespaces
Alert if any returns "no"
2. Certificate expiry monitoring
certmanager_certificate_expiration_seconds
Alert 15 days before expiry
3. Controller forbidden errors (zero tolerance)
apiserver_request_total{code="403", user=~"system:serviceaccount:.*"}
Alert on > 0
4. Watch stream collapse
apiserver_request_total{verb="LIST"} spike
Alert on 3x baseline within 5 minutes
5. Probe failures
rate(kubelet_started_pods_total{result="failed"}[5m])
Alert on > 0.1 per 5 minutes
6. Storage exhaustion
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes
Alert at 80% capacity
7. Control plane latency
apiserver_request_duration_seconds_sum / apiserver_request_duration_seconds_count
Alert when p99 > 1 second
8. Logging pipeline lag
loki_logql_querieslatency_seconds
Alert when > 5 seconds
```