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.