Context
DevOps career switch in progress, minimal 2‑node k3s homelab in La Celle-Saint-Cloud. I publicly document every architecture decision — both to clarify my own choices and to show a future employer that I know how to operate a Kubernetes cluster in real conditions.
Today’s topic: how to connect 2 k3s nodes without hacking together a site-to-site VPN, without custom router tables, and without paying for a cloud.
Hardware
Two heterogeneous machines:
Server node: old Ivy Bridge desktop, Intel i7-3770K (2012), 32 GB RAM, RTX 2060 12 GB for future PyTorch workloads. Ethernet connection to a 1 Gb switch.
Worker node: GEEKOM A8 Mini PC, AMD Ryzen 7 8745HS (Zen 4, 8c/16t, 28 W TDP), 32 GB RAM, 1 TB NVMe SSD. Ethernet on the same switch.
Laptop: HP Pavilion Gaming running Ubuntu 24.04, my main workstation.
Both nodes run Ubuntu 24.04.4 LTS, kernel 6.17, k3s v1.35.4, containerd runtime 2.2.3.
Intentionally asymmetric topology: the server gets the GPU for ML, the worker remains a general-purpose compute node for CI/CD and stateless apps.
The problem: accessing the cluster from anywhere without exposing the LAN
When I work from home, my laptop is on the same LAN as both nodes — easy. But as soon as I move (coffee shop, trip), I want to be able to:
- Run
kubectl get podsfrom anywhere - SSH into the nodes to debug
- Without opening port 6443 or port 22 on my home router
Three serious options:
Manual site-to-site VPN like WireGuard: homegrown config, key generation, peer propagation, MTU tuning. Feasible but time‑consuming.
Publicly exposed bastion: one node accessible via public SSH, the rest behind it. Increases the attack surface, not interested.
Managed mesh VPN: Tailscale, Twingate, NetBird. Tailscale is free up to 100 personal devices, based on WireGuard under the hood, and installs in 5 minutes.
I chose Tailscale.
On each machine (server, worker, laptop):
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh
The --ssh flag enables SSH managed by Tailscale (authentication via Tailscale ACLs rather than scattered SSH keys).
The direct 192.168.1.x:41641 is important: on the same LAN, Tailscale detects peers and does not go through DERP relays. Latency and bandwidth = native LAN. Off the LAN, it routes via DERP (Tailscale-hosted) with a bit more latency but still usable.
Immediate bonus: MagicDNS resolves ml-hellomichka and hellomichka-a8 everywhere, no more memorizing IPs.
The counterintuitive decision: Tailscale only for the control plane
The classic “Tailscale + k3s” homelab mistake you see online: forcing all Kubernetes traffic (Flannel CNI, kubelet, etcd) to go through Tailscale via --flannel-iface=tailscale0 and --node-ip=100.x.x.x.
In my setup, I chose not to do that. The proof in kubectl get nodes -o wide: the INTERNAL-IP values are my LAN IPs (192.168.1.52, 192.168.1.103), not my Tailscale IPs (100.x.x.x).
Why?
My two nodes are physically on the same 1 Gb switch. The Kubernetes data plane (pod‑to‑pod traffic, kubelet‑to‑API‑server) benefits from native throughput, without WireGuard overhead or reduced MTU.
If Tailscale goes down or Tailscale’s control plane has an incident (rare but it happens), my cluster keeps running. The decoupling of the admin control plane / K8s data plane is intentional.
Tailscale remains useful for what it’s best at: giving me encrypted access to my cluster from anywhere, without hacks.
Concretely, from my laptop, my ~/.kube/config points to https://100.117.114.43:6443. Port 6443 on the server is exposed only on the tailscale0 interface. Nobody on the internet can reach the API server.
The limits of this choice
To be honest, this setup has limitations you need to be aware of:
If I add a third node outside the LAN (at a friend’s house, on a cloud), I will have to switch to
--flannel-iface=tailscale0because the nodes will no longer be able to reach each other directly.No encryption of intra‑cluster network traffic. For a personal homelab, that’s fine. For multi‑site production, it is not acceptable.
MagicDNS does not mix with the cluster’s CoreDNS. My pods resolve their services via Kubernetes CoreDNS, my laptop resolves hostnames via MagicDNS. No cross‑pollution, but this needs to be understood.
What’s next
Article 2 planned for next week: deploying Traefik with cert-manager to expose a service in HTTPS via an external domain, without ugly NodePorts or cloud LoadBalancers. The real question: how to publish my homelab apps on the internet cleanly when you have a dynamic residential IP.
The GitHub repo accompanying this series is coming — I did not want to publish it before having a first article setting the stage.
To follow the series: free Substack subscription. For technical feedback: LinkedIn or GitHub (link in the bio).
Top comments (0)