My homelab cluster runs in a closet at home. I do not. I work from coffee shops, from a client office, sometimes from a hotel with WiFi that feels actively hostile. And I still want to reach my own machines while I’m out there.
For years the standard answer to that was to poke holes in your own front door. Forward a port on the router, wire up dynamic DNS so the changing home IP doesn’t break everything, write firewall rules, and then sit with the quiet hope that nobody scanning the internet stumbles onto the SSH daemon you just exposed. It works, in the sense that you can reach your stuff. It also means a part of your private network is now answering questions from strangers, all day, forever.
So the question I actually wanted answered: how do I reach my homelab from anywhere without leaving anything open to the internet? That’s the gap Tailscale closes for me.
What Tailscale Actually Is
Tailscale is a mesh VPN built on top of WireGuard. Every device you enrol gets a stable address in the 100.x.y.z range, and every device can talk directly to every other one. There’s no central box your traffic has to flow through, so this isn’t the old “VPN concentrator that everything tunnels back to” model.
flowchart TD
subgraph ts["Tailscale Network (100.x.y.z)"]
Laptop["Laptop<br/>100.64.0.1"]
Phone["Phone<br/>100.64.0.2"]
Homelab["Homelab<br/>100.64.0.3"]
VPS["VPS<br/>100.64.0.4"]
end
Laptop <--> Phone
Laptop <--> Homelab
Laptop <--> VPS
Phone <--> Homelab
Phone <--> VPS
Homelab <--> VPS
When the network cooperates, two devices connect straight to each other. When NAT is being difficult (and it often is), traffic falls back to relaying through Tailscale’s DERP servers. You don’t manage any of that. Either path is encrypted end to end, so even the relay can’t read what passes through it.
The honest trade-off, before we go further: the coordination plane is Tailscale’s, not yours. They run the control servers that hand out keys and tell devices how to find each other. The actual traffic stays peer to peer and encrypted, but the connection brokering depends on a company you don’t host. I’ll come back to that, because for a blog built on sovereignty it matters, and there’s a fix.
Why I Reach for It Anyway
The thing that sold me is what disappears from my router config. Nothing is forwarded. Nothing is exposed. There is no port for a scanner to find, which means there is no service I have to keep hardened against the entire internet. The attack surface I used to maintain just stopped existing.
It also punches through network setups that would otherwise be a dead end. CGNAT from an ISP that won’t give you a real public IP, double NAT, the locked-down WiFi at a hotel: Tailscale gets through all of it, because both ends reach out rather than waiting to be reached. My homelab is just there, on my tailnet, regardless of what mess of routers sits between us.
A few smaller wins stack up on top of that. Each device keeps the same 100.x.y.z address, so I’m not chasing dynamic DNS records that lag behind reality. MagicDNS gives every machine a name like homelab.tail-abc123.ts.net, so I stop memorising IPs. And ACLs let me say who can reach what, which means a guest device or a borrowed laptop doesn’t automatically get the keys to the cluster.
Getting It Running
Start with the homelab node itself.
# Ubuntu/Debian
curl -fsSL https://tailscale.com/install.sh | sh
# Start Tailscale
sudo tailscale up
# Check status
tailscale status
tailscale up prints an authentication link. Open it, log in, and the node joins your tailnet. Then put the client on whatever you carry around:
- Your laptop
- Your phone
- Anything else that needs in
Every device authenticates against the same Tailscale account, and once they have, they can see each other. To check it’s working, hit the homelab from your laptop:
# From laptop
ping homelab.tail-abc123.ts.net
# SSH to homelab
ssh user@homelab.tail-abc123.ts.net
# Access services
curl http://homelab.tail-abc123.ts.net:3000 # Grafana
No port forwarding, no firewall rules to write. The first time you SSH into a machine at home from a train, having opened nothing, it feels slightly illegal.
Reaching the Devices That Can’t Run Tailscale
Plenty of things on a home network will never run a Tailscale client. The NAS firmware won’t allow it, the printer is a printer, the smart plug is barely a computer. A subnet router fixes that by turning one enrolled node into a doorway to the rest of the LAN.
# On homelab node
sudo tailscale up --advertise-routes=192.168.1.0/24
# Accept the route in Tailscale admin console
# Or use --accept-routes on clients
After you accept the advertised route, your laptop can reach 192.168.1.50 (the NAS, the printer, whatever lives there) by routing through the homelab node.
flowchart LR
subgraph remote["Remote (Coffee Shop)"]
Laptop["Laptop<br/>100.64.0.1"]
end
subgraph home["Home Network"]
Homelab["Homelab<br/>100.64.0.3<br/>192.168.1.10"]
NAS["NAS<br/>192.168.1.50"]
Printer["Printer<br/>192.168.1.60"]
end
Laptop -->|Tailscale| Homelab
Homelab -->|Local| NAS
Homelab -->|Local| Printer
Sending All Traffic Home
There’s a related trick for when the network around you can’t be trusted at all. Turn a home node into an exit node and your laptop can route everything through it, so your traffic leaves from your home IP instead of the café’s.
# On homelab node
sudo tailscale up --advertise-exit-node
# Accept in admin console
# On laptop (when needed)
sudo tailscale up --exit-node=homelab
I flip this on when I’m on WiFi I don’t control, which gets me out from under whatever that network is logging. It’s also handy for reaching geo-restricted things as if you were home, and for using your home Pi-hole no matter where you are.
Putting Cluster Services on the Tailnet
This is where it gets genuinely nice for a Kubernetes homelab. The Tailscale Kubernetes Operator can publish services straight onto your tailnet, so they’re reachable by name without an ingress facing the internet.
# Install operator
helm repo add tailscale https://pkgs.tailscale.com/helmcharts
helm repo update
helm install tailscale-operator tailscale/tailscale-operator \
--namespace tailscale \
--create-namespace \
--set oauth.clientId="<client-id>" \
--set oauth.clientSecret="<client-secret>"
Create the OAuth client in the Tailscale admin console first, then drop those credentials into the install. With the operator running, exposing a service is one annotation:
apiVersion: v1
kind: Service
metadata:
name: grafana
annotations:
tailscale.com/expose: "true"
spec:
selector:
app: grafana
ports:
- port: 3000
Grafana is now reachable at grafana.tail-abc123.ts.net:3000 from any device on the tailnet, and nowhere else. If you’d rather treat Tailscale as a proper ingress controller, that works too:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
spec:
ingressClassName: tailscale
rules:
- host: grafana
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000
That gives you grafana.tail-abc123.ts.net with HTTPS handled for you.
Deciding Who Can Reach What
Enrolling devices is the easy half. The half worth thinking about is access. By default everything on your tailnet can talk to everything else, and that’s fine until you add a device you trust less. ACLs let you tighten it.
{
"acls": [
// Admin can access everything
{
"action": "accept",
"src": ["group:admin"],
"dst": ["*:*"]
},
// Developers can access cluster services
{
"action": "accept",
"src": ["group:dev"],
"dst": ["tag:k8s:80,443,6443"]
},
// Everyone can access monitoring
{
"action": "accept",
"src": ["*"],
"dst": ["tag:monitoring:3000,9090"]
}
],
"groups": {
"group:admin": ["user@example.com"],
"group:dev": ["dev1@example.com", "dev2@example.com"]
},
"tagOwners": {
"tag:k8s": ["group:admin"],
"tag:monitoring": ["group:admin"]
}
}
You tag devices in the admin console, and the ACL decides what each tag can reach. A dev box gets the cluster API and the web ports, the monitoring stack is readable by anyone on the tailnet, and admin gets the rest. The same principle that makes network policies worth writing applies here: a smaller blast radius when something does go wrong.
Names and Certificates
MagicDNS already gives every device a device-name.tail-abc123.ts.net name. In the admin console you can enable it and swap the random tailnet string for something you’ll actually remember.
For TLS, Tailscale will issue real certificates for tailnet domains:
tailscale cert grafana.tail-abc123.ts.net
That’s valid HTTPS for an internal service without dragging Let’s Encrypt or cert-manager into the picture. If you want your own domain in front, point a CNAME at the tailnet name and tell Tailscale to accept it:
# In your DNS
grafana.internal.example.com CNAME grafana.tail-abc123.ts.net
When You Do Need Something Public
Occasionally you want one thing reachable from the open internet: a webhook endpoint, a link to hand someone for an hour, a quick demo. Tailscale Funnel gives you a public URL for that without touching your router.
# Expose local port 3000 publicly
tailscale funnel 3000
You get back something like https://homelab.tail-abc123.ts.net/. One warning worth taking seriously: Funnel sits outside your ACLs. Whatever you funnel is genuinely public, so treat it like you’ve forwarded a port after all, and only funnel things you’re happy for the world to poke at.
Owning the Control Plane
Back to that trade-off from the start. The thing that nags at me about depending on Tailscale’s coordination servers is the same thing that nags at me about any black box: if it’s down, or if the company changes, my access changes with it. For a homelab built on the idea of owning what you run, handing the keys-and-discovery layer to someone else is a real concession.
Headscale is the answer to that. It’s an open-source reimplementation of the Tailscale control server that you host yourself, and the standard Tailscale clients talk to it happily.
# headscale deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: headscale
spec:
replicas: 1
selector:
matchLabels:
app: headscale
template:
spec:
containers:
- name: headscale
image: headscale/headscale:latest
args:
- serve
ports:
- containerPort: 8080
volumeMounts:
- name: config
mountPath: /etc/headscale
- name: data
mountPath: /var/lib/headscale
Same WireGuard underneath, same client experience, except now the coordination runs on infrastructure you control. You take on running and updating it yourself, which is the cost. What you get back is a mesh that nobody else can pull the plug on.
Wiring It Into the Rest of Your Tools
A couple of integrations earn their keep day to day. Tailscale can handle SSH auth, so you stop juggling keys for hosts that are already on your tailnet:
# Enable Tailscale SSH on server
tailscale up --ssh
# Connect (no keys needed!)
ssh user@homelab.tail-abc123.ts.net
Tailscale authenticates the connection using your tailnet identity, so there’s no key to copy around. Cluster access works the same way once the API server is on the tailnet:
# ~/.kube/config
apiVersion: v1
clusters:
- cluster:
server: https://k3s.tail-abc123.ts.net:6443
name: homelab
contexts:
- context:
cluster: homelab
user: admin
name: homelab
current-context: homelab
kubectl from a hotel room hits the cluster as if you were sitting next to it. And for editing on the box itself, a one-line SSH config entry is enough for VS Code Remote to connect:
// .ssh/config
Host homelab
HostName homelab.tail-abc123.ts.net
User youruser
What My Setup Actually Looks Like
Nothing exotic. One node does the heavy lifting and my handful of devices connect to it.
flowchart TD
subgraph tailnet["My Tailnet"]
subgraph home["Home"]
K3s["K3s Cluster<br/>Subnet Router<br/>Exit Node"]
NAS["Synology NAS"]
end
subgraph devices["Devices"]
Laptop["Laptop"]
Phone["Phone"]
Tablet["Tablet"]
end
end
Laptop --> K3s
Phone --> K3s
Tablet --> K3s
K3s --> NAS
The K3s node acts as subnet router for 192.168.1.0/24, so the NAS and everything else on the LAN are reachable. It’s also my exit node for the days I’m on WiFi I’d rather not trust. The Tailscale Operator publishes Grafana, ArgoCD, and GitLab onto the tailnet, and an ACL keeps the whole thing scoped to my own devices.
The payoff is mundane in the best way. I check monitoring from anywhere. I’ve kicked off an ArgoCD sync from my phone on a train. And the router has never had a single port forwarded to make any of it work.
When It Misbehaves
A few things that have actually tripped me up.
“Tailscale is blocked.” Some corporate networks block it outright. You can move it to a different DERP port, or run it over TCP/443 so it looks like ordinary HTTPS traffic and slips past.
“The connection is slow.” Usually that means the direct path failed and you’re going through a DERP relay. Check whether a firewall is eating the UDP traffic Tailscale uses for direct connections, and whether your NAT type is one that forces relaying.
“I can’t reach the subnet.” Subnet routes have to be accepted, not just advertised. Advertise on the router node with --advertise-routes, accept the route in the admin console, and enable --accept-routes on the clients that need it.
Why This Sticks
The shift Tailscale makes is small to describe and large in practice. Instead of opening your network and then defending the opening, you open nothing, and reaching anything requires authenticating onto the mesh first. Encryption isn’t a setting you remember to turn on, it’s the default, and access lives in one place you can reason about.
What I actually get out of it is a homelab that stays private while behaving, from my side, as if I never left the house. With Headscale in the mix I get that without leaning on anyone else’s servers to make the connections happen. The most secure port really is the one that was never open.
