deploy · tutorial · vps

Tailscale Deploy Trick: Close Port 22 Forever

Put SSH, admin pages, and deploys inside a private WireGuard mesh. Port 22 closes to the world in 15 minutes. Guide inside.

A three-headed dog, Cerberus, crouching in a dark cave, watercolor by William Blake, about 1824
A three-headed dog, Cerberus, crouching in a dark cave, watercolor by William Blake, about 1824 · Photo: William Blake, public domain, via Wikimedia Commons

Port 22 is a public punching bag

Spin up any VPS with SSH on the default port and check auth.log a day later. Thousands of login attempts from IPs you will never visit, trying root, admin, ubuntu, and every password from every breach list.

Keys-only auth survives that noise. But the noise never stops, one config mistake reopens the door, and every open port is a promise you must keep forever: patched, watched, and correct on every future rebuild.

The tailscale deploy private pattern ends the vigil. SSH stops listening to the planet and starts answering only your devices.

Public SSH vs mesh SSH

Concern Port 22 open to world Tailscale mesh only
Who can knock 8 billion IPs, all day Only devices on your Tailnet
Brute-force log noise Thousands of lines daily Zero — no public listener
Auth material Key files scattered per device Identity-bound, per person
Revoking access Edit authorized_keys everywhere One click in admin panel
New device setup Copy keys, open firewall, pray Sign in, done — same hostname

Three details make public SSH worse than the log lines:

  1. One flag undoes everything. PasswordAuthentication yes in a pasted tutorial, a stale ubuntu account from the image, a forwarded agent on a cafe network — any single slip turns noise into entry.
  2. Keys sprawl silently. Laptop, old laptop, CI runner, contractor from March — each holds a key that still works. Nobody audits authorized_keys until after the incident.
  3. Firewalls drift. You open 22 "temporarily" for a rescue, forget the rule, and the temporary becomes permanent. The mesh has no temporary rules because there are no port rules at all.

Cloudflare Tunnel already hides your app traffic. The mesh hides your admin traffic. Together the box has no public doors left.

Your private deploy mesh

BoringPush documents this in its VPS guide. Fifteen minutes, and port 22 closes for good.

Step 1 — Join the server and your devices to one Tailnet.

# on the VPS
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname=prod-box

# firewall: only the mesh may administer
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
sudo ufw enable

Phone and laptop install Tailscale, sign into the same account, and prod-box resolves everywhere — couch, 5G, airport Wi-Fi. Same hostname, every network, no dynamic-DNS dance. SSH to an IP never again.

Step 2 — Lock the mesh with tags, not trust.

In the Tailscale admin panel, tag the server and write the smallest ACL that works:

{
  "tagOwners": { "tag:prod": ["[email protected]"] },
  "acls": [{ "action": "accept", "src": ["autogroup:member"], "dst": ["tag:prod:22"] }]
}

Members reach port 22 on production and nothing else. Add a co-founder and they inherit exactly that — no key files to pass around, no authorized_keys to edit. Remove them and access dies in one click, including sessions from a lost phone. Identity replaces key sprawl.

Step 3 — Run deploys and admin through the mesh only.

git remote add production som@prod-box:/var/repos/app.git
ssh som@prod-box "pm2 ls && free -h"

Pushes, logs, and reboots all travel inside WireGuard encryption to a hostname scanners cannot see. Disable key expiry for prod-box itself (an expired server key is a self-inflicted lockout), keep a second admin device on the Tailnet as a spare key, and turn off any direct-SSH firewall rule you added during setup week.

That's the architecture talking — the admin road is private, the public road shows nothing.

The outside-knock test

Check Command (from outside the Tailnet) Pass looks like
SSH unreachable nc -vz YOUR_VPS_IP 22 Connection refused
No leaked admin curl -m 5 http://YOUR_VPS_IP:3000 Timeout / refused
Mesh alive tailscale status on phone prod-box listed
Funnel off Admin panel → prod-box No funnel badge anywhere
Spare access Second device ssh som@prod-box Connects in seconds

Admitting the tax: Tailscale is one more account and one more daemon. If their control plane has a bad day, new device approvals wait — existing connections keep flowing on cached keys, but onboarding pauses.

BoringPush accepts that because the alternative is a public port knocked on by the whole internet. Keep one emergency path (your provider's web console) and the tradeoff stays theoretical.

FAQ

Does a tailscale deploy private setup slow down my pushes?

No — WireGuard adds roughly a millisecond on nearby regions, invisible next to a 20-second build. Most founders report pushes feel faster, because the direct encrypted path replaces whatever the cafe Wi-Fi was doing to port 22. The bottleneck is npm run build, never the mesh.

What happens if Tailscale's servers go down — am I locked out?

Existing node-to-node connections keep working on cached keys. Only new logins and ACL changes pause. For true emergencies your VPS provider's web console still opens a shell outside the mesh. Keep that console login in a password manager and a second admin device on the Tailnet, and an outage becomes an inconvenience.

Can my co-founder and I share one Tailnet safely?

Yes — that is what ACLs are for. Tag production boxes, grant members SSH only to those tags, and each person signs in with their own identity. When someone leaves, remove them once in admin and every door closes. No hunting for their key across five servers.

Should I still use SSH keys inside the mesh?

Yes. Tailscale proves which device connects; keys prove which user on it. --ssh mode can even check Tailnet identity directly, but keys cost nothing and survive a mesh misconfiguration. Two light locks beat one heavy one.

How is this different from a VPN like WireGuard by hand?

Tailscale is WireGuard, minus the weekend of key generation, peer configs, and NAT traversal debugging. Hand-rolled WireGuard works and costs $0, but every new phone means editing configs on the server. The mesh trades a free account for zero config maintenance. Purists can self-host Headscale later — the firewall posture stays identical.

Does the mesh cover my database too?

It covers anything bound to localhost or the Tailnet. SQLite needs no port at all — a file the app reads directly. If you run Postgres for a second service, bind it to the Tailscale IP instead of the public one and only mesh members connect. Same rule everywhere: public binds are the bug.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading