# 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.

Source: https://boringpush.com/blog/tailscale-wireguard-deploy-private

## 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.**

```bash
# 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:

```json
{
  "tagOwners": { "tag:prod": ["you@example.com"] },
  "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.**

```bash
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

- [Cloudflare Tunnel + Next.js: Hackers Find Zero Open Ports](/blog/cloudflare-tunnel-nextjs-vps) — the visitor-side half of the closed box
- [git push production deploy: Skip the 9-Minute CI Queue](/blog/git-push-production-main-deploy) — the push that travels inside this mesh
- [Deploy Next.js on Hetzner $5 by Dinner Tonight](/blog/deploy-nextjs-hetzner-vps) — the afternoon build the mesh protects
