Remote access: Tailscale, with WireGuard as break-glass¶
Remote access to the homelab runs over Tailscale. Nothing listens on the WAN for it. Every device makes outbound connections to the tailnet, and a small subnet-router VM on Proxmox advertises the LAN. The UniFi gateway's built-in WireGuard server is the emergency fallback for when Proxmox itself is down. It is the only inbound port, and nothing else uses it.
The router is a VM outside the Kubernetes cluster, so remote access continues
to work while the cluster is in an upgrade or broken. It is a VM and not an
LXC because Tailscale needs /dev/net/tun. The bpg provider cannot pass that
device through to a container.
What Terraform declares¶
terraform/proxmox/hyper-cluster/tailscale/ owns the Tailscale side.
The router VM is vmid 140 on hyper1 at 10.3.10.40. It boots from the Debian 13
genericcloud image. Cloud-init installs Tailscale and joins the tailnet with an
auth key from the same plan (reusable = false, 90 day expiry, pre-authorized,
tagged tag:subnet-router).
tailscale_acl owns the entire tailnet policy file: the tag definitions, an
autoApprovers rule that approves the advertised route with no console click,
the access grants, and the Tailscale SSH rule. Policy changes happen in
Terraform. A console edit is drift, and the next apply overwrites it.
Split DNS sends local.bigd.no to the UniFi gateway (10.3.10.1) for tailnet
clients, so internal hostnames work remotely.
terraform/unifi/network/remote-access.tf owns the emergency fallback: the
UniFi WireGuard server on UDP 51820, and the Cloudflare dynamic DNS records
(ddns.bigd.no, ddns.nordbye.it) that keep the WAN address reachable by name.
Bootstrap that cannot be declared¶
The tailnet itself is created by hand at signup under the identity
morten@nordbye.it. Tailscale has no email accounts. The tailnet depends on an
identity provider, so that provider's MFA is the network security.
On a fresh tailnet, before the first terraform apply:
- In the admin console policy file, add
tag:subnet-routertotagOwners. The tag must exist before an OAuth client can be scoped to it. - Create an OAuth client (Settings, Trust credentials) with write scope on
Auth Keys (
tag:subnet-router), Policy File and DNS. Put its id and secret in the root's gitignoredterraform.tfvars. - Import the policy:
terraform import tailscale_acl.tailnet acl. The provider refuses to overwrite a non-default policy. - Set
proxmox_ssh_passwordinterraform.tfvars. The cloud-init snippet upload goes over SSH, and the hyper nodes do not trust the laptop's key.
Operations¶
- Rebuilding the VM: the join key is single-use and expires after 90 days.
Replace it together with the VM with
terraform apply -replace=tailscale_tailnet_key.router. Tagged devices themselves never key-expire. - First boot takes a few minutes (apt update, Tailscale install, then
tailscale up). The machine then shows in the admin console astailscale-routerwith its route already approved. - The join key is in the Terraform state and in the cloud-init snippet on Proxmox storage. It is single-use, tag-scoped and short-lived. The state already holds more sensitive material (the Talos PKI).
- The cloud image and the snippet live on the shared
nfs-vmstore. The VM disk is on hyper1'slocal-lvm. - Emergency fallback: keep a WireGuard profile installed on the phone and test it from cellular at intervals.
- Tailnet lock is off. With it on, every Terraform-created node would need a
manual
tailscale lock signfrom a trusted device.