SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

What is Tailscale and how does it work?

Tailscale explained: WireGuard tunnels between your servers, a coordination server for keys and ACLs, NAT traversal, DERP relays, and the threat model.

What is Tailscale?

Tailscale is a VPN that connects your machines straight to each other instead of routing all traffic through one gateway you run. Every node runs WireGuard, so packets travel encrypted from one server to another and nothing in the path can read them. A hosted coordination server handles the introductions. It stores and distributes public keys and tells each node where the others are. It also pushes down the access rules you wrote.

That split is the entire design. The data plane is peer to peer and encrypted between nodes. The control plane is a service Tailscale runs for you. Every interesting question about Tailscale, including the uncomfortable ones about trust, follows from those two facts. If you have already built a WireGuard VPN by hand on a VPS, Tailscale is that same tunnel with key distribution and firewall traversal done for you.

How does Tailscale work?

Your private network of nodes is called a tailnet. Four things happen when a machine joins one.

  1. The tailscaled daemon starts, generates a WireGuard key pair, and keeps its state in /var/lib/tailscale/tailscaled.state. The private key stays on that machine. Tailscale's own wording is blunt: "the private key never, ever leaves its node."
  2. The node logs in to the coordination server and uploads its public key, plus the addresses where it believes it can be reached. Tailscale describes that server as "a shared drop box for public keys."
  3. The coordination server sends back a network map: the public key, the tailnet address, the machine name and the candidate endpoints of every node this one is allowed to reach.
  4. Each pair of nodes then tries to build a direct WireGuard tunnel between themselves. When that fails, they push the packets through a relay instead.

Each node gets a stable address out of 100.64.0.0/10, the carrier-grade NAT range that runs from 100.64.0.0 to 100.127.255.255. Tailscale uses that range because it is reserved for provider infrastructure, so it rarely collides with the private addresses your servers already use. On Linux the tunnel appears as an interface named tailscale0.

The WireGuard implementation lives inside tailscaled in userspace, not in the kernel module. That is why Tailscale starts on container virtualisation where sudo modprobe wireguard fails with Operation not supported. It also means the throughput ceiling on a given box is lower than kernel WireGuard, which is one of the trade-offs Tailscale against plain WireGuard goes into.

Two commands tell you where you stand.

tailscale ip -4
tailscale status

tailscale status prints one line per node, and the last column is the one that matters.

100.101.102.103  web-1      you@  linux  -
100.101.102.104  db-1       you@  linux  active; direct 198.51.100.24:41641
100.101.102.105  ci-runner  you@  linux  active; relay "fra"

direct followed by an address and port means the two machines found a path to each other and traffic is peer to peer. relay "fra" means it is passing through a Tailscale relay in Frankfurt. A - means there is no active session with that node right now, which is normal.

What the coordination server can and cannot see

The coordination server holds public keys and metadata. It knows your machine names, which user or tag owns each node, each node's tailnet address, the public addresses your nodes are reachable at, when each was last online, and the policy file you wrote. That is a complete map of your fleet.

It holds no private key, so it cannot decrypt traffic between two nodes. Encryption is end to end between the WireGuard peers, and the coordination server is not a peer.

What it can do is hand out keys. Any coordination server, hosted or self-run, is trusted to tell your nodes which public keys belong in the tailnet. That is the hinge of the threat model further down, and the reason Headscale, an open-source coordination server you host yourself exists.

How two servers behind different firewalls talk directly

NAT (network address translation) is what lets many machines share one public address. Your VPS usually has a public address of its own, but the other machines you want in the tailnet often do not: a home server, a build runner on an office network, a box behind a provider firewall you cannot edit.

Tailscale finds a path using techniques built on the STUN (session traversal utilities for NAT) and ICE standards. Each node sends a small UDP packet to a STUN server and learns the public address and port its router assigned to that socket. Both nodes report those candidates to the coordination server, which passes them to the other side. Then both nodes start sending packets to each other at the same time. Each router sees an outbound packet first, so it creates a mapping and accepts the reply arriving from that same address. Neither side needed an inbound firewall rule.

The ports are specific. Direct WireGuard tunnels use UDP with a source port that defaults to 41641. STUN runs over UDP 3478 to Tailscale's relay servers. The control connection and any relayed data use HTTPS on TCP 443. Most of the time you open nothing inbound, though on a network with a difficult NAT, allowing UDP 41641 inbound makes a direct connection more likely.

tailscale netcheck

Read two lines of that report. UDP: true means UDP leaves the machine at all, and UDP: false means every connection from this node will be relayed. MappingVariesByDestIP: true means the router assigns a different public port per destination, so the address prediction above cannot work and those nodes usually stay relayed.

When Tailscale uses a DERP relay instead

DERP (designated encrypted relay for packets) is the fallback. Tailscale runs relays in many regions, reachable over TCP 443, and a node that cannot get a direct path sends its WireGuard packets through one.

The packets stay encrypted. Tailscale states it plainly: "there is never a way for a DERP server to decrypt your traffic. It just blindly forwards already-encrypted traffic from one node to another." A relay sees ciphertext, and it sees which node is talking to which.

Relays also carry the first packets of most connections. Finding a direct path takes a moment, so a session often starts relayed and upgrades in place once the two nodes locate each other. You can watch that happen.

tailscale ping db-1

The first replies come back via DERP(fra), then a later line reports something like via 198.51.100.24:41641. That change is the upgrade to a direct tunnel. If it never changes, run tailscale netcheck on both ends. A relayed path still works. It costs latency, because every packet takes a detour through a third machine.

Joining a VPS to your tailnet

The install script covers Ubuntu and Debian.

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

sudo tailscale up prints a URL. Open it, authenticate, and the node appears in your admin console. Then confirm the daemon comes back after a reboot, because that is the step people skip.

sudo systemctl is-enabled tailscaled
tailscale status

is-enabled should print enabled, and tailscale status should list the new node with its 100.x address. For a server built from a script, an interactive URL is no use. Generate an auth key in the admin console and pass it, with a tag that records what kind of machine this is.

sudo tailscale up --auth-key=tskey-auth-REPLACE-ME --advertise-tags=tag:server

A tagged node is owned by the tag rather than by the person who ran the command, so it keeps working after that person's account is gone. The tag must be declared in your policy file under tagOwners first, or the command is refused.

Two settings matter for a fleet. Node keys expire after 180 days by default (as of August 2026), and when a key expires "connections to/from the given endpoint will stop working" until someone logs in again, so open the machine's row in the admin console and choose Disable Key Expiry on unattended servers. MagicDNS, enabled by default for tailnets created on or after 20 October 2022, gives each node a name such as db-1.yak-bebop.ts.net, resolved by a stub resolver at 100.100.100.100. Use the names rather than the addresses, because a rebuilt node gets a new address and keeps its name.

If the install itself fails on apt or on the repository, the common Tailscale install errors on Ubuntu covers the fixes.

Reaching a service bound to localhost

This is where a tailnet becomes useful, and where people get stuck. Joining the tailnet does not make a loopback service reachable.

ss -tlnp | grep 3000

If that prints 127.0.0.1:3000, the socket accepts only packets whose destination is 127.0.0.1. A request from another node arrives addressed to this node's 100.x address, so the kernel has nothing listening for it and answers with a TCP reset. The client reports Connection refused. The tunnel is fine. The listener is the problem.

There are two honest fixes. Bind the service to the node's tailnet address, which keeps it off the public interface with no proxy in the way: pass --bind 100.101.102.104 or the equivalent option in your config, and for a container publish the port as -p 100.101.102.104:3000:3000. Or leave the service on loopback and put Tailscale in front of it.

tailscale serve 3000

That proxies requests to http://127.0.0.1:3000 and serves them inside your tailnet on a ts.net name over HTTPS, once HTTPS certificates are enabled for the tailnet. It stays private to your nodes. The public version of the same idea is Funnel, and Tailscale serve against funnel covers which one you want.

Two related jobs have their own pages. Reaching a whole private network that has no Tailscale installed on it needs a subnet router on a VPS, and sending a node's outbound internet traffic through another node needs an exit node.

Closing the ports you no longer need

Once every administrator reaches the box over the tailnet, public port 22 has no job left. That is the practical payoff: a port that is closed cannot be brute forced, and your logs stop filling with attempts.

Order matters. Add the tailnet access, prove you can log in through it from a second session, and only then remove the public rule.

sudo ufw allow in on tailscale0
sudo ufw status verbose

After that, delete the public SSH rule and reconnect using the MagicDNS name. Note what ufw allow in on tailscale0 really does: it trusts everything arriving on the tunnel, so your Tailscale policy file becomes the access control instead of ufw. Write the policy with that in mind.

One warning for anyone running containers. A published Docker port installs its own NAT rules and bypasses ufw, so a ufw deny does not close it. Docker published ports bypassing ufw explains the mechanism. Publishing to the tailnet address, as above, sidesteps it.

What Tailscale protects, and what it does not

Worth stating plainly, because the marketing version blurs the line.

Protected: traffic between two nodes is encrypted end to end with WireGuard, and no relay in the middle can read it. Private keys never leave the machine that generated them. Nodes need no inbound public port, so there is nothing on 22 or 5432 for the internet to scan. Access between nodes is decided by a policy file rather than by whoever knows an address.

Not protected: the coordination server sees your device graph. That metadata is sensitive on its own, because machine names, owners, addresses and online times describe your infrastructure. It also distributes keys, which is the sharper risk. Tailscale says it directly: "If Tailscale were malicious, and stealthily inserted new nodes into your network, then Tailscale could send or receive traffic to your existing nodes in plaintext." Your single sign-on provider sits in the same trust path, because whoever can mint an identity there can add a node. And a compromised node is a peer inside the tailnet, so what it reaches next is whatever your policy allows.

There are two answers to the key distribution risk. The first is tailnet lock, which requires existing trusted nodes to cryptographically sign a new node before your other nodes accept it. A control plane that adds a node without a valid signature is ignored. The admin console generates the exact tailscale lock init line for your signing nodes, and every node can confirm what it sees.

tailscale lock status

All nodes should report the same set of trusted signing keys. The second answer is to run the control plane yourself. A self-hosted Headscale coordination server speaks the same protocol to the same clients, which moves the device graph and the key distribution onto hardware you own. You then own that server's uptime as well.

One default to fix on the first day. A new tailnet ships permissive: "the default tailnet policy file enables communication between all devices within the tailnet." As soon as you add an acls section, the model flips to deny by default and only your rules pass.

{
  "tagOwners": {
    "tag:server": ["autogroup:admin"]
  },
  "acls": [
    {"action": "accept", "src": ["autogroup:member"], "dst": ["tag:server:22"]}
  ]
}

That policy lets tailnet members reach SSH on tagged servers and nothing else. Add a rule per service rather than leaving the wildcard in place, because the wildcard means one stolen laptop key reaches your database.

Failure modes and the strings you will see

tailscale status always says relay. The two nodes never built a direct path. Run tailscale netcheck on both ends. UDP: false means UDP is blocked outbound, so only a relay can work. MappingVariesByDestIP: true means a hard NAT is in the way, and allowing UDP 41641 inbound on the side you control often fixes it.

A node that worked for months disappeared. Its node key expired at the 180 day default. The machine shows as expired in the admin console, and sudo tailscale up on the box brings it back. Disable key expiry on servers so it does not repeat.

Peers are listed but connections time out. Connectivity is working and the policy is denying the traffic. Check the acls section for a rule covering this source, destination and port. A denied packet is dropped rather than answered, which is why you get a timeout instead of Connection refused.

MagicDNS names do not resolve. ping db-1 fails while ping 100.101.102.104 works. Something replaced /etc/resolv.conf, so queries never reach the stub resolver at 100.100.100.100. Check cat /etc/resolv.conf for 100.100.100.100, and look at whatever else on the box writes that file. It is the same class of problem as DNS breaking inside a WireGuard tunnel.

tailscale up refuses your tag. The tag is not declared under tagOwners in the policy file. Add it there, then run the command again.

FAQ

Is Tailscale a VPN or a mesh network?

Both words are accurate, and they describe different layers. The tunnels are WireGuard, which makes it a VPN. The topology is a mesh, because each node builds a tunnel directly to each node it talks to instead of sending every packet through one central server. The coordination server sits in the control path, not in the data path, so if it becomes unreachable your existing tunnels keep carrying traffic. What stops during an outage is new nodes joining and changes to keys or policy landing.

Can Tailscale read my traffic?

Not the contents. Traffic is encrypted end to end between nodes with WireGuard, private keys never leave the nodes, and a DERP relay forwards packets it has no way to decrypt. Tailscale does see metadata: machine names, owners, public keys, endpoint addresses and when each node is online. It also distributes keys, so a compromised coordination server could try to insert a node your fleet would then trust. Tailnet lock blocks that by requiring signatures from your own trusted nodes, and Headscale removes the hosted control plane from the picture.

Do I need to open firewall ports for Tailscale?

Almost never inbound. Tailscale's own guidance is that "most of the time, you don't need to open any firewall ports." Outbound, a node needs TCP 443 to the coordination server and the relays, plus UDP 3478 for STUN. Direct tunnels use UDP with a source port that defaults to 41641. Allowing UDP 41641 inbound is optional, and it only helps direct connections succeed on awkward networks.

Why can other nodes not reach my service on port 3000?

Check the bind address first with ss -tlnp. A listener on 127.0.0.1:3000 refuses connections that arrive addressed to the node's 100.x tailnet address, because that socket accepts only the loopback destination, and the client sees Connection refused. Bind the service to the tailnet address, or run tailscale serve 3000 to proxy it. If the listener is already on 0.0.0.0 and the connection times out instead of refusing, the cause is a policy rule or a host firewall rather than the bind address.

Should I run Headscale instead of Tailscale's coordination server?

Run Headscale when the device graph or the key distribution must stay on infrastructure you control, or when the tailnet has to work with no dependency on an outside service. The clients and the protocol are the same. The cost is that you now operate the coordination server, and its outage stops new nodes joining and stops policy changes from applying. For a small fleet, the hosted control plane with tailnet lock enabled is usually the better trade.