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.
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 statuson both devices shows the other as onlinessh 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>:22from 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 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
No comments yet
Be the first to weigh in.