Directions
1Sign up and create the tailnet
Sign in at tailscale.com with an identity provider you already run (Google, Microsoft, GitHub). Your tailnet — the private network — is born from that identity, which is the point: access is tied to SSO accounts you can revoke, not shared keys.
The free tier covers personal use and small teams generously; you can put off pricing thought until you're onboarding a real team.
2Install on each machine
Install the client on laptops, servers, and phones and run tailscale up to authenticate each one. Every node gets a stable 100.x.y.z IP that follows it across networks — coffee shop, home, cloud region, doesn't matter.
For servers and containers where interactive login is awkward, generate an auth key in the admin console (tag it, make it ephemeral for CI runners) and pass it with --auth-key. Traffic is end-to-end WireGuard; direct peer-to-peer when possible, relayed when NAT is hostile.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --auth-key tskey-auth-...
3Turn on MagicDNS
Enable MagicDNS in the admin console and machines become reachable by name — ssh ben@backup-server instead of memorizing 100.x addresses. Enable HTTPS certificates too and Tailscale will mint real certs for tailnet hostnames, which makes internal web UIs (like that Uptime Kuma instance) properly TLS'd with zero config.
4Write ACLs before you need them
By default every node can reach every node — fine for one person, wrong for a team. The ACL file (JSON in the admin console) grants access by user and tag: tag servers by role, then write rules like "group:dev can reach tag:staging on 22,443; only admins reach tag:prod."
Do this while the network is five machines, not fifty. Tailscale SSH is worth enabling at the same time — SSH access controlled by the same ACLs, with session recording available.
"acls": [
{ "action": "accept", "src": ["group:dev"],
"dst": ["tag:staging:22,443"] },
{ "action": "accept", "src": ["group:admin"],
"dst": ["tag:prod:*"] }
] 5Close the public ports
The payoff step: with everything reachable over the tailnet, remove the public ingress you no longer need. SSH on port 22 to the world, the admin dashboard on a high port, the database bound to 0.0.0.0 — close them at the cloud firewall and bind services to the tailscale interface where possible.
For sharing a single internal service outward, use Tailscale Funnel (public) or node sharing (specific external users) rather than reopening ports.
Common issues & fixes
Two nodes can't reach each other.
Check tailscale status — if the connection says 'relay', hard NAT is forcing DERP (slower but works). If it fails entirely, an ACL is blocking it; test with the ACL preview tool in the console.
Auth key stopped working in CI.
Keys expire and single-use ephemeral keys are consumed per run. Generate a reusable ephemeral key for runners, and rotate it like any credential.
MagicDNS names don't resolve on one machine.
The OS DNS override didn't take — usually a VPN or corporate DNS fighting for control. Toggle 'Override local DNS' in that client's settings.
Server unreachable after closing port 22.
Confirm tailscaled is enabled at boot (systemctl enable tailscaled) before closing the public door — otherwise a reboot locks you out until you use the cloud console.
Node keys expire and servers drop off.
Default key expiry is 180 days. Disable expiry per-node for servers in the admin console — that's what the setting is for.
Honesty note: articles are drafted with AI assistance, then a human
checks each one against the vendor's current setup flow before it
publishes — nothing goes live unreviewed. Vendor interfaces still
change; if a step looks different, the underlying record is what
matters.