The cluster experienced a widespread outage where most apps that used NFS-backed PVCs could not start (ContainerCreating / mount timeouts). The underlying issue was asymmetric routing between the Talos node and the TrueNAS host caused by Tailscale on the NAS accepting subnet routes that were being advertised from the homelab tailnet.
This prevented the Kubernetes node from completing TCP handshakes to the NAS (UI/API on 80/443 and NFS on 2049), which in turn caused democratic-csi to never become ready and blocked provisioning/mounting.
Observed in the cluster:
democratic-csi-*-controllerinCrashLoopBackOff.- Sidecars failing probe with:
TrueNAS api is unavailableAxiosError: timeout of 5000ms exceeded(duringGET /api/v2.0/system/version/)
- Many pods stuck in
ContainerCreatingdue to PVC mount failures (NFS-backedtruenas-*StorageClasses). - From inside the cluster, connections to the NAS (
10.0.0.210) timed out even though other LAN hosts (for example the router10.0.0.1) were reachable.
This homelab uses a Tailscale subnet router in Kubernetes:
infrastructure/tailscale-subnet-router/connector.yamladvertises single-host routes such as:10.0.0.197/32(Talos node / Kubernetes API)10.0.0.231/32(MetalLB ingress IP)10.0.0.210/32(NAS)10.0.0.1/32(router)
The NAS also runs Tailscale for remote access.
If the NAS has "Accept Routes" enabled, it will import subnet routes from the tailnet and install policy routing (Linux routing rules and routes, often in a dedicated table such as 52). On a LAN where the NAS can also reach those same IPs directly, this can unintentionally cause the NAS to prefer sending replies via tailscale0 instead of the LAN interface.
The Talos node (10.0.0.197) could send packets to the NAS (10.0.0.210) over the LAN, but the NAS was returning the replies over Tailscale instead of the LAN. That breaks TCP handshakes and makes the NAS appear "down" from the node.
- Node sends SYN to
10.0.0.210:443over LAN. - NAS replies SYN-ACK back to
10.0.0.197over LAN. - Connection establishes,
democratic-csican probe the TrueNAS API, and NFS mounts work.
- Node sends SYN to
10.0.0.210:443over LAN. - NAS selects a tailnet route for
10.0.0.197/32and sends SYN-ACK viatailscale0instead of LAN. - Node never receives the reply on its LAN socket, so the connection times out.
flowchart LR
subgraph LAN["LAN (10.0.0.0/24)"]
Node["Talos node\n10.0.0.197"]
NAS["TrueNAS\n10.0.0.210"]
end
subgraph Tailnet["Tailnet (Tailscale)"]
TSNode["NAS tailscale0\n100.68.x.x"]
SubnetRouter["homelab-subnet-router\n(advertises 10.0.0.197/32, 10.0.0.231/32, ...)"]
end
Node -->|"SYN (LAN)"| NAS
NAS -. "policy route\n(to 10.0.0.197 via tailscale0)" .-> TSNode
TSNode -->|"SYN-ACK (Tailnet)"| Node
The fastest signal was in the controller sidecars:
kubectl -n democratic-csi get pods -o wide
kubectl -n democratic-csi logs <controller-pod> -c external-provisioner --tail=200The logs consistently showed probe failure because the driver could not reach the TrueNAS API within the probe timeout (5 seconds for /system/version).
Connectivity checks from both:
- a normal pod network IP (
10.244.x.x), and - hostNetwork (
10.0.0.197)
showed the NAS timing out, while another LAN host (router 10.0.0.1) responded normally.
Example probes:
ping -c 1 -W 1 10.0.0.210
nc -vz -w 2 10.0.0.210 80
nc -vz -w 2 10.0.0.210 443
nc -vz -w 2 10.0.0.210 2049
curl -m 5 -sS -o /dev/null -w "api %{http_code} total=%{time_total}\n" http://10.0.0.210/api/v2.0/system/version/On the Talos node interface, tcpdump showed SYN packets leaving the node, but no corresponding SYN-ACK coming back.
On TrueNAS, tcpdump on the LAN NIC showed the ICMP echo requests and SYN packets arriving from 10.0.0.197, but no replies on the same interface. This is consistent with replies being routed via another interface (in this case tailscale0).
The key confirmation was:
ip route get 10.0.0.197
ip rule
ip route show table 52Before the fix, ip route get 10.0.0.197 resolved via tailscale0 (table 52) with a tailnet source IP, showing that the NAS would send return traffic for 10.0.0.197 over Tailscale.
Also confirmed via Tailscale localapi prefs:
PID="$(pgrep -x tailscaled)"
curl --unix-socket "/proc/$PID/root/var/run/tailscale/tailscaled.sock" \
http://local-tailscaled.sock/localapi/v0/prefs | grep -E '"RouteAll"'which reported RouteAll: true (accepting routes).
In the TrueNAS SCALE UI for the Tailscale app, turn off:
Accept Routes
This prevents the NAS from importing tailnet subnet routes and avoids accidental route preference over the LAN.
PID="$(pgrep -x tailscaled)"
curl -sS -X PATCH --unix-socket "/proc/$PID/root/var/run/tailscale/tailscaled.sock" \
-H 'Content-Type: application/json' \
-d '{"RouteAll":false,"RouteAllSet":true}' \
http://local-tailscaled.sock/localapi/v0/prefsVerify on the NAS:
ip route get 10.0.0.197Expected: dev enp1s0 src 10.0.0.210 (LAN), not dev tailscale0 table 52.
Once node to NAS connectivity is restored, forcing the controller pods to restart is the fastest way to clear the crashloop state:
kubectl -n democratic-csi delete pod \
-l app.kubernetes.io/component=controller-linux,app.kubernetes.io/instance=democratic-csi-hdd
kubectl -n democratic-csi delete pod \
-l app.kubernetes.io/component=controller-linux,app.kubernetes.io/instance=democratic-csi-nvmedemocratic-csi is sensitive to NAS API reachability at startup:
- The controller sidecars probe the CSI driver.
- The driver
Probevalidates TrueNAS API connectivity by calling/api/v2.0/system/version/. - In the
nextimage, that call uses a hard-coded 5 second timeout.
So any routing break that makes the NAS look unreachable reliably keeps the controller in a permanent probe failure loop.
Cluster:
kubectl -n democratic-csi get podsshows controllers6/6 Running.- New pods previously stuck on NFS PVCs move past
ContainerCreating.
Network:
- From the Talos node context,
ncto10.0.0.210:2049succeeds. - From the NAS,
ip route get 10.0.0.197prefers LAN (enp1s0).
- Keep TrueNAS Tailscale "Accept Routes" disabled unless you have a specific routing requirement.
- If you ever need the NAS to accept routes for another reason, add a guardrail to force LAN routing for
10.0.0.0/24and ensure it persists across reboots (platform-specific). - If the issue repeats after an app upgrade/restart, re-check:
- TrueNAS Tailscale app settings (
Accept Routes) ip route get 10.0.0.197on the NAS
- TrueNAS Tailscale app settings (
- Monitoring now includes
PrometheusRule/monitoring/truenas-storage-alertsto catch:- unavailable
democratic-csicontrollers - unavailable
democratic-csinode plugins - cluster impact where multiple
truenas-*PVC-backed pods remain unready
- unavailable
Later, the NAS failed again with a different signature:
- the host still answered ping and NFS/RPC on
111and2049 - the TrueNAS UI/API on
80/443refused connections democratic-csicrashed because its probe tohttp://10.0.0.210:80/api/v2.0/system/version/failed
On-box evidence showed this was not pool corruption:
zpool statusreportedboot-pool,nvme_pool, andtankallONLINEnginx.servicehad been killed by the Linux OOM killermiddlewaredhad grown above 2 GiB RSS on a host with about 3.7 GiB usable RAM- kernel logs repeatedly showed
nfsd invoked oom-killerwith stack frames in:nfsd_copy_file_rangenfsd4_copy
That points to a memory-starvation event where NFSv4 server-side copy activity was the trigger, and the management plane (nginx / TrueNAS API) was one of the casualties. When the API dies, democratic-csi becomes unavailable and PVC-backed workloads get stuck on restart.
- Keep unused NAS services off. During the incident,
iscsitargetwas enabled even though it had zero configured targets, extents, or target-extents. - Preserve userspace headroom on low-memory TrueNAS hosts by capping ARC instead of leaving
zfs_arc_maxunlimited. - Avoid negotiating generic
nfsvers=4from Kubernetes clients on this cluster.nfsvers=4allows Linux clients to negotiate up to4.2; pinning tonfsvers=4.1avoids thenfsd4_copypath implicated in this outage pattern. - Monitoring now also probes the TrueNAS management plane and NFS port directly, so alerting can fire before democratic-csi fan-out becomes the first obvious symptom.
- Prometheus also scrapes the NAS Netdata endpoint on
${NAS_IP}:6999viaServiceMonitor/monitoring/truenas-netdata. Grafana dashboardTrueNAS Host Overviewtracks available memory, ARC size, and per-service memory fornginx,middlewared, and Netdata. Netdata is bound to the NAS LAN IP in/etc/netdata/netdata.conf; the pre-change backup from the 2026-04-26 incident follow-up is/etc/netdata/netdata.conf.codex-backup-20260426-oom-monitoring. TrueNASManagementPlaneDownButNFSUpis the high-signal alert for this failure mode: it fires when the API probe is down while NFS still accepts connections.TrueNASNginxServiceNotReported,TrueNASMemoryHeadroomLow, andTrueNASMiddlewareMemoryHighprovide earlier or more specific signals when host metrics are still scrapeable.CronJob/democratic-csi/truenas-management-plane-refreshproactively restartsmiddlewared,nginx, and Netdata from Kubernetes weekly at04:00every Monday. It has a one-hour start deadline so a missed run does not execute much later outside the maintenance window. It uses the SOPS-manageddemocratic-csi/truenas-ssh-credentialsSecret and opens only a short SSH session to the NAS, avoiding any additional resident process on the TrueNAS host.- The
Jobstab inhttps://controlpanel.khzaw.devshows this CronJob's schedule, suspend state, recent run statuses, and log tails. It can also commit narrow schedule/timezone/suspend changes back to GitOps and create a manual run from the CronJob template.
The democratic-csi storage classes now use:
nfsvers=4.1
for both HDD-backed and NVMe-backed TrueNAS NFS storage classes. Existing dynamically provisioned PVs should also be patched in-cluster so future remounts use the same version.