Skip to content
Security Beginner Tutorial

Zero Trust Remote Access to Your Home Lab with Tailscale

Replace your open SSH port with a private WireGuard mesh, then lock port 22 down for good.

Emeka Okafor
Emeka Okafor
Security Editor · Jul 14, 2026 · 9 min read
Zero Trust Remote Access to Your Home Lab with Tailscale

What you'll build

A private mesh network (a "tailnet") connecting your laptop to a home lab server or VPS over WireGuard, with SSH access authorized by device identity instead of an open port. By the end, port 22 is closed to the public internet, and you can still SSH in from anywhere.

Prerequisites

  • A server you control: home lab box, Raspberry Pi, or VPS running Ubuntu 22.04+/Debian 11+ (other distros work, adjust package manager commands)
  • A laptop or desktop running macOS, Linux, or Windows
  • Root or sudo access on the server
  • A free Tailscale account (Personal tier, up to 100 devices, no credit card)
  • 20 minutes, and a second terminal session to the server open as a safety net (don't skip this)

Step 1: Create your tailnet

Go to tailscale.com and sign up with Google, GitHub, Microsoft, or a passkey. Tailscale doesn't run its own username/password auth, it rides on an identity provider you already trust. This creates your tailnet, a private namespace all your devices join.

Step 2: Install Tailscale on your server

SSH in the normal way (you're not locking that down yet) and run the official install script:

curl -fsSL https://tailscale.com/install.sh | sh

This detects your distro and adds Tailscale's apt/yum repo instead of dumping a raw binary on you, so updates come through your normal package manager from here on.

Step 3: Authenticate the server

sudo tailscale up

This prints a login URL like https://login.tailscale.com/a/xxxxxxxxxxxx. Open it in a browser on any device (your laptop is fine) and approve it. The command exits once auth completes, and the server shows up in your tailnet.

Check its assigned address:

tailscale ip -4

You'll get an address in the 100.x.y.z range (Tailscale's carved-out slice of CGNAT space). That's a stable, private IP that follows the device no matter what network it's actually on.

Step 4: Install Tailscale on your laptop

  • macOS: brew install --cask tailscale, then launch it and click "Log in" in the menu bar icon, or grab the installer from tailscale.com/download.
  • Linux: same install script as above, then sudo tailscale up.
  • Windows: download the installer from tailscale.com/download and log in through the tray app.

Log in with the same account you used for the server.

Step 5: Confirm the mesh works

On your laptop:

tailscale status

You should see both devices with their 100.x.y.z addresses and hostnames. Ping the server by name (MagicDNS gives you a plain hostname without memorizing the IP):

tailscale ping myserver

A response means you've got an encrypted, authenticated tunnel between the two devices, no VPN concentrator, no port forwarding. Note that tailscale ping resolves through Tailscale's own daemon, not your OS resolver, so it working doesn't guarantee plain ssh will resolve the same name.

Step 6: Enable Tailscale SSH in your ACL policy

This is the step people skip and then lock themselves out over. On a new tailnet, Tailscale SSH is disabled by default. Turn on --ssh on the server and close public SSH without setting this first, and nothing reaches the box at all.

Open the Tailscale admin console, go to Access Control, and add an ssh block:

{
 "acls": [
 {"action": "accept", "src": ["*"], "dst": ["*:*"]}
 ],
 "ssh": [
 {
 "action": "accept",
 "src": ["autogroup:member"],
 "dst": ["autogroup:self"],
 "users": ["autogroup:nonroot", "root"]
 }
 ]
}

autogroup:member means any device belonging to a user on your tailnet; autogroup:self restricts the destination to that same user's own devices. That's the right default for a solo home lab. Click Save, it takes effect immediately, no restart needed.

Step 7: Turn on Tailscale SSH

sudo tailscale up --ssh

This starts a small SSH server inside tailscaled itself, bound to the tailnet interface. It's independent of your system's sshd, which matters later when you firewall port 22.

From your laptop:

ssh you@myserver

Tailscale checks the session against the ACL you just saved, layered on top of whatever key/password auth your OS normally does.

If myserver doesn't resolve, that's a common MagicDNS gap, especially on Linux where systemd-resolved doesn't always pick up Tailscale's search domain cleanly. Fall back to the IP or the full FQDN, both visible in tailscale status:

ssh you@100.x.y.z
ssh you@myserver.your-tailnet-name.ts.net

Use the IP if you don't trust DNS resolution on the client, the FQDN if it works and you want something memorable.

Step 8: Kill public SSH access

With ufw on the server, check what's currently allowed first:

sudo ufw status numbered

Then lock it down:

sudo ufw allow in on tailscale0 to any port 22
sudo ufw delete allow OpenSSH
sudo ufw deny 22/tcp
sudo ufw enable

(If your existing rule isn't named "OpenSSH," delete it by number instead, using the numbers from ufw status numbered.) Order matters: the tailscale0 allow rule has to exist before the blanket deny, since ufw matches rules top-down and stops at the first hit. Keep your original session open and test a fresh connection over Tailscale before closing anything.

If your server is a VPS, also lock down the provider's firewall (AWS security group, DigitalOcean firewall, Hetzner firewall) and restrict inbound 22, or delete the rule. ufw only controls the host firewall, the cloud firewall sits in front of it and needs its own lockdown.

Verify it works

  • tailscale status on both devices shows the other as online
  • ssh you@myserver (or the IP/FQDN if the short name doesn't resolve) succeeds over the tailnet
  • From a network not on your tailnet, ssh you@<public-ip> times out or is refused
  • curl -m 5 <public-ip>:22 from an outside network hangs or fails

If all four hold, port 22 is dead to the internet and alive to your tailnet.

Troubleshooting

tailscale: command not found after install: open a new shell, the install script updates PATH but your current session doesn't know yet.

SSH connection refused even with --ssh enabled: confirm the ACL policy actually saved with an ssh block. An empty or missing ssh array means Tailscale SSH is off no matter what the server-side flag says.

Locked out after the firewall change: this is why you kept a second session open. If you didn't, most VPS providers offer a browser-based console (serial/VNC) that bypasses SSH entirely, use it to run ufw disable and start over.

tailscale ping times out or devices can't reach each other: check tailscale status for relay vs direct. Restrictive corporate or CGNAT networks sometimes force traffic through Tailscale's DERP relays, which still works but adds latency. Nothing broken, just NAT traversal falling back.

ssh you@myserver hangs on "Could not resolve hostname": the MagicDNS/systemd-resolved gap from Step 7. Use tailscale ip -4 on the server, or the full .ts.net FQDN.

Next steps

Look into ACL tags and grants in the admin console to scope access per-device instead of letting the whole tailnet reach everything, useful once you add more people or IoT-ish devices. Tailscale Funnel is worth a look if you want to expose one service publicly without opening a port. And if you'd rather run your own control plane instead of trusting Tailscale's, Headscale is the open-source, self-hosted alternative speaking the same client protocol.

Emeka Okafor
Written by
Emeka Okafor · Security Editor

Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading