Why the NAS Tunnel Couldn't Reach the VM

Adding lab.tinkerer.tools to the NAS cloudflared tunnel looked like a dashboard click — two stacked TrueNAS networking constraints made it impossible.

I thought adding lab.tinkerer.tools to the existing NAS cloudflared tunnel would take five minutes: new public hostname, new Access application, done. It took an afternoon. The goal was a second browser-terminal path alongside ssh.tinkerer.tools — ttyd with JetBrainsMono Nerd Font injected via nginx sub_filter, so starship and LazyVim glyphs render in any browser. The NAS’s cloudflared cannot reach the VM — not via LAN IP, not via Tailscale. When the homelab gets nested, plausible plans fail for reasons invisible at the dashboard.

The diagnostic that flipped the plan

From the NAS shell, curl -sI --max-time 5 http://100.98.79.48:7682/ hung and returned nothing. Then:

ip link show tailscale0 2>&1 | head -3
# 279: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 …

tailscale status
# -bash: tailscale: command not found

The interface is there. The CLI isn’t. The Tailscale catalog app runs network_mode: host, so it creates tailscale0 on the NAS host — but the daemon state lives inside the container. The host namespace has no peer routes. cloudflared also runs host-network, inherits that namespace, and can reach neither address.

Two stacked constraints, not a config mistake

  1. TrueNAS uses macvtap VM networking by default, which deliberately blocks host↔VM traffic on the same NIC. (Documented in Homelab Dev VM.)
  2. Tailscale — the intended bridge — isn’t on the NAS host. It’s in an app container. The peer routes don’t exist where cloudflared runs.

Either issue alone is fixable. Stacked, the NAS side is a dead end.

Fix: run cloudflared on the VM

The VM reaches its own localhost:7682. The NAS tunnel keeps serving ssh/vault/watch/request.tinkerer.tools; a new VM-local tunnel serves lab.tinkerer.tools.

cloudflared tunnel login
cloudflared tunnel create lab-vm
cloudflared tunnel route dns lab-vm lab.tinkerer.tools

# write ~/.cloudflared/config.yml with tunnel UUID, then:
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/config.yml
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo ~ is /root

cloudflared service install searches /etc/cloudflared and the invoking user’s home. Under sudo, that’s /root — not ~ignacio. Copy first or the installer fails silently.

Other gotchas

ProblemCauseFix
Script exits silently at ttyd installgrep "x86_64$" never matches — JSON lines end with "grep 'ttyd\.x86_64"$'
login: Cannot possibly work without effective rootlogin requires root for PAM; ttyd runs unprivilegedzsh -l in ExecStart; Access PIN handles auth
curl localhost:7682 refused after changing listen addresssystemctl reload doesn’t rebind socketssystemctl restart nginx
Blank screen after tunnel connectsAccess app was SSH type — injects Cloudflare’s own terminal, conflicts with ttydDelete and recreate as Self-hosted

The two browser paths

Both gate with a Cloudflare Access one-time PIN to info@tinkerer.tools:

  • ssh.tinkerer.tools — Cloudflare’s browser SSH client. No font control. Good for quick commands from any device.
  • lab.tinkerer.tools — ttyd with Nerd Font. Use for LazyVim, starship, lazygit — anything where tofu boxes break the workflow.

Native SSH remains the primary path. These two are the escape hatches.