Citra and Rowan run a two-node k3s cluster (v1.36.2+k3s1) over Tailscale, with Rowan as control-plane and Citra as worker/agent. Both nodes run WSL2 Ubuntu 26.04 with networkingMode=mirrored.
Tailscale runs natively inside each WSL2 instance (not just on the Windows host), giving each node its own tailscale0 interface and Tailscale device identity. This is what k3s and flannel bind to for all cluster traffic.
| Node | Role | WSL2-native Tailscale device |
|---|---|---|
| Citra | Worker/agent | citra-wsl |
| Rowan | Control-plane | rowan-wsl |
Note: the Windows hosts also have their own separate Tailscale device identities (citra and rowan), used for SSH access to Windows itself. Cluster traffic uses the WSL2-native identities above, not these.
Both nodes' k3s services are configured with explicit --node-ip and --flannel-iface flags pointing at the WSL2-native Tailscale interface (tailscale0), rather than relying on auto-detection.
Rowan (/etc/systemd/system/k3s.service):
ExecStart=/usr/local/bin/k3s \
server \
'--node-ip' \
'<ROWAN_WSL_TAILSCALE_IP>' \
'--tls-san' \
'<ROWAN_WSL_TAILSCALE_IP>' \
'--flannel-iface' \
'tailscale0' \
Citra (/etc/systemd/system/k3s-agent.service):
ExecStart=/usr/local/bin/k3s \
agent \
'--node-ip' \
'<CITRA_WSL_TAILSCALE_IP>' \
'--flannel-iface' \
'tailscale0' \
Citra's agent join config (/etc/systemd/system/k3s-agent.service.env):
K3S_URL=https://<ROWAN_WSL_TAILSCALE_IP>:6443
K3S_TOKEN=<node-token, from Rowan: /var/lib/rancher/k3s/server/node-token>
Local kubectl (Mac) is configured against https://<ROWAN_WSL_TAILSCALE_IP>:6443.
Postgres runs off-cluster on Faraday, a Termux phone host that is not a k3s node. It replaced Rowan's systemd Postgres (postgresql@18-main) in October 2026. Full setup, hardening, and operations are on Faraday Node (Termux Postgres Host).
Config:
listen_addresses = '*' in Faraday's postgresql.confpg_hba.conf allows the Tailscale CGNAT range, since pod traffic reaches Postgres via the node's Tailscale IP as the source address: