Solo operator field reference

TailscaleExpress working manual

A practical guide to running a personal tailnet across your own devices and servers—built for setup day, the first outage, and every small decision in between.

Scope
Solo operator / homelab. Personal tailnet, your own devices, servers you control.
Source basis
Tailscale documentation, fetched and checked in August 2026.
Outside scope
Organization management, PAM, AI governance, Kubernetes, and enterprise logging.
01Working manual

The mental model

Tailscale is a private mesh network laid over the internet. Every device you sign in gets a stable private IP, and any two of your devices can talk directly regardless of NAT, firewalls, or which continent they're on. No port forwarding.

Four pieces, and the split between them is the thing worth understanding:

Tailnet — your network. "An interconnected collection of users, devices, and resources." Created the moment you first sign in. A personal tailnet is a shared-domain tailnet (a gmail.com login); an organization tailnet uses a custom domain.

Control plane — the coordination server at controlplane.tailscale.com. It distributes public keys, assigns IPs, holds your access policy, tells devices about each other, and helps them find each other through NAT. It never carries your traffic.

Data plane — WireGuard tunnels running on each device. Encrypt, decrypt, forward. Private keys are generated locally and never leave the device.

DERP — "Designated Encrypted Relay for Packets." Globally distributed relays that do two jobs: help devices negotiate a connection, and relay encrypted packets when a direct path is impossible. Relayed traffic is still end-to-end encrypted; the relay forwards ciphertext it cannot read.

Connection preference order: direct → peer relay → DERP.

What Tailscale can and cannot see

Cannot: your traffic. "Devices running Tailscale only exchange their public keys. Private keys never leave the device. All traffic is end-to-end encrypted, always."

Can (control plane metadata): device names, Tailscale IPs, client versions, OS, approximate geographic location, public keys, your DNS and policy config. From your identity provider it requests your email and name.

Linux clients also send connectivity logs by default. Kill them with TS_NO_LOGS_NO_SUPPORT=true in /etc/default/tailscaled — but the docs warn this "may prevent Tailscale from providing technical support."

If the coordination server goes down

Existing connections keep working — keys are cached locally, firewall rules are enforced on-device. What breaks: adding devices, key refresh, policy updates, revocation. Over time devices lose access to each other as keys expire.

02Working manual

Addresses

IPv4: 100.64.0.0/10 (RFC 6598 CGNAT range) — 100.64.0.0 through 100.127.255.255. IPv6: fd7a:115c:a1e0::/48.

Addresses are auto-assigned and stable for the life of the registration: "A node's IP address will not change for as long as the node remains registered unless an Admin changes it." Re-registration — removing the device from the Machines page, reinstalling, or losing the node key — gets you a new one.

Reserved — never assign these:

AddressPurpose
100.100.100.100Quad100 — device-local. DNS resolver on port 53, device management UI on port 80
fd7a:115c:a1e0::53IPv6 Quad100
100.100.0.0/24, 100.100.100.0/24, 100.115.92.0/23Tailscale internal
100.101.102.103The tshello example service

Find yours: tailscale ip (all), tailscale ip -4 (IPv4 only).

The CGNAT collision you will eventually hit: some ISPs also use 100.64.0.0/10, and so do some other VPNs. iOS and Android enforce one VPN at a time. If you must run two VPNs on desktop, either use Tailscale's userspace networking mode or add 100.64.0.0/10 and fd7a:115c:a1e0::/48 to the other VPN's split-tunnel config. That workaround does not work with exit nodes — exit nodes support one VPN at a time, full stop.

03Working manual

Day one

Install

Linux

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

Prints a URL. Open it, authenticate, done. Needs the tun kernel module (sudo modprobe tun; persist in /etc/modules-load.d/tun.conf).

macOS — Monterey 12.0+. Standalone build from pkgs.tailscale.com/stable/#macos is the recommended variant. Install the VPN configuration when prompted, sign in with your IdP. Note that neither the App Store nor the Standalone build can act as a Tailscale SSH server — only the open-source tailscaled variant can. The App Store build also lacks the tailscale ssh subcommand (use plain ssh).

Windows — Windows 10+ / Server 2016+. One .exe from tailscale.com/download/windows handles both 32- and 64-bit. Tray icon → Log in.

iOS — 15.0+. App Store. Allow the VPN config and push notifications; notifications are how you learn a key is about to expire.

Android — 8.0+. Play Store. For Android TV without a keyboard: admin console → Machines → Add device → Client device → Android → Input code tab.

Static binaries (odd architectures, no package manager):

shell
tar xvf tailscale_<version>_<architecture>.tgz
sudo tailscaled --state=tailscaled.state
sudo tailscale up

Sign-in

There are no Tailscale passwords. You authenticate through an identity provider: Apple, Google, GitHub, Microsoft, Okta, OneLogin, a custom OIDC provider, or a passkey for a tailnet you're already authorized to join.

Pick carefully. You cannot migrate a tailnet to or from GitHub or Apple as the IdP.

Free plan

Signing up with a public-domain email (@gmail.com) puts you on Personal: 6 free users, 50 tagged resources, 3 ACL groups. A custom domain starts a 14-day trial of the paid tier instead. The docs don't state a device-count cap anywhere; the pricing page says user devices are unlimited. Treat that as lower confidence than a docs claim.

Firewall

Usually nothing to do. If you want to improve your odds of direct connections, allow outbound: TCP to *:443, UDP from source port 41641 to anywhere, UDP to *:3478 (STUN). To accept inbound peer connections: sudo ufw allow 41641/udp.

04Working manual

The CLI you'll actually use

The flag-persistence trap

tailscale up does not remember flags. "Flags are not persisted between runs; you must specify all flags each time." Run tailscale up --ssh today and plain tailscale up tomorrow, and SSH is off.

tailscale set is the modern, incremental alternative — it changes only what you name and leaves everything else alone. Use set for configuration; use up for connecting and authenticating. Current docs have migrated most feature examples from up to set.

To clear a flag, pass it empty: tailscale set --exit-node=.

Reference

CommandDoesFlags worth knowing
tailscale upConnect + authenticate--auth-key=, --force-reauth, --accept-routes, --ssh, --shields-up, --advertise-routes=, --advertise-exit-node, --timeout=, --accept-risk=lose-ssh|all
tailscale downDisconnect (stays logged in)--reason="...", --accept-risk=
tailscale setChange one setting, persistently--ssh, --exit-node=, --advertise-routes=, --advertise-exit-node, --accept-routes, --accept-dns, --hostname=, --operator=, --shields-up, --auto-update, --webclient (port 5252)
tailscale statusWho's up, direct vs relayed--json, --active, --peers, --web
tailscale ipThis device's addresses-4, -6, --1
tailscale ping <host>Tailscale-aware ping w/ path detail--until-direct, --tsmp, --icmp, --peerapi, --c, --timeout=
tailscale netcheckPhysical network diagnosis--every=, --format=json, --verbose
tailscale file cp/getTaildropsee §7
tailscale serveExpose a local service to the tailnetsee §7
tailscale funnelExpose a local service to the internetsee §7
tailscale cert <fqdn>Get a Let's Encrypt cert--cert-file=, --key-file=, --min-validity=, --serve-demo
tailscale ssh <user@host>SSH over the tailnet
tailscale exit-node listAvailable exit nodes--filter=<country>
tailscale logoutDisconnect + expire the loginRemoves an ephemeral node immediately
tailscale switchMultiple accounts--list
tailscale bugreportSupport identifier--diagnose, --record
tailscale whois <ip[:port]>Identity, tags, capabilities of a peer--json (must precede the IP)
tailscale lockTailnet Lockinit, status, add, remove, sign

CLI location by platform. Linux: in $PATH. macOS standalone: Settings → CLI integration → Show me how → Install Now (needs Ventura 13.0+; installs to /usr/local/bin/tailscale). macOS App Store build: /Applications/Tailscale.app/Contents/MacOS/Tailscale <command>. Windows: run the .exe from Command Prompt. iOS and Android: no CLI at all.

05Working manual

Names: MagicDNS, tailnet name, HTTPS

MagicDNS

Automatic DNS for every device. ssh me@monitoring instead of ssh me@100.87.12.4. Enabled by default on tailnets created on or after 20 October 2022; otherwise there's an Enable MagicDNS button at login.tailscale.com/admin/dns. No nameserver required on v1.20+.

FQDN is <machine-name>.<tailnet-dns-name> — e.g. monitoring.yak-bebop.ts.net. Tailscale adds search domains, so ping monitoring and ping monitoring.yak-bebop.ts.net are equivalent.

Two exceptions:

  • Devices shared to you from another tailnet must use the full FQDN. Bare hostname won't resolve.
  • On macOS, host and nslookup bypass system DNS and will not see MagicDNS. Use dscacheutil -q host -a name my-servername there is a literal keyword, not a placeholder. On Windows use Resolve-DnsName, not nslookup (it ignores NRPT rules). On Linux nslookup is fine.

You cannot add arbitrary records to MagicDNS. Open issue, no workaround.

Turn it off per-device: tailscale set --accept-dns=false (Linux); macOS menu bar → Preferences → uncheck Use Tailscale DNS settings; Windows SHIFT + right-click the tray icon → deselect Use Tailscale DNS.

Tailnet name

New tailnets get tail<hex>.ts.net. You can rename — but only to another Tailscale-generated random name like cat-crocodile.ts.net, never an arbitrary string. Admin console → DNS → Rename tailnet.

Warning: once you use a randomized name for HTTPS certificates you cannot re-roll it. You can only toggle between that name and the default. Renaming breaks existing MagicDNS links, HTTPS URLs, and shares.

HTTPS certificates

  1. Admin console → DNS → enable MagicDNS → HTTPS Certificates → Enable HTTPS.
  2. On each machine: tailscale cert <machine>.<tailnet>.ts.net

Certs come from Let's Encrypt via DNS-01; Tailscale creates the TXT record. Your cert key and ACME account key are generated and stored locally — Tailscale never sees them.

Three things that bite:

  • Machine names get published to public Certificate Transparency logs. Don't enable this if any hostname is sensitive. Only machines where you actually run tailscale cert get published.
  • You renew it yourself. 90-day expiry. tailscaled doesn't know where you installed the file, so nothing auto-renews unless the consumer is integrated (Caddy, Serve).
  • No bare-hostname HTTPS. https://monitoring will never work. https://monitoring.yak-bebop.ts.net and http://monitoring do.

Re-requesting too often hits Let's Encrypt rate limits: "you may find yourself waiting 34 hours until you can try again."

06Working manual

Reaching things: subnet routers and exit nodes

Two different jobs, constantly confused.

Subnet router = reach specific networks that can't run Tailscale (printers, IoT, a NAS, a whole VPC). Exit node = route all internet traffic through a device. The traditional-VPN behavior.

Subnet router

On the router device:

shell
sudo tailscale set --advertise-routes=192.0.2.0/24,198.51.100.0/24

Windows PowerShell drops the sudo. macOS without the CLI on $PATH: /Applications/Tailscale.app/Contents/MacOS/Tailscale set --advertise-routes=....

Comma-separated list. All platforms except Apple TV support IPv6 subnets. Don't advertise 0.0.0.0/0 here — that's what exit nodes are for.

Enable IP forwarding (Linux) — required, and easy to forget:

shell
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

If /etc/sysctl.d doesn't exist, write to /etc/sysctl.conf and sysctl -p that instead. On firewalld systems also run firewall-cmd --permanent --add-masquerade.

Approve the routes. Admin console → Machines → filter property:subnet → select the device → Subnets → Edit → check the routes → Save. Nothing works until you do this. Automate it with autoApprovers (§9).

Client side. Android, iOS, macOS, tvOS and Windows pick up approved routes automatically. Linux does not — Linux clients only learn Tailscale IPs by default:

shell
sudo tailscale set --accept-routes

SNAT. On by default: traffic from behind the router appears to come from the router. Disable with tailscale up --snat-subnet-routes=false (Linux only) to preserve source IPs — but then you need a return route to 100.64.0.0/10 pointing at the router's LAN IP, configured on each destination device or in DHCP/VPC settings. Known bug: --snat-subnet-routes=false on a node that is both subnet router and exit node causes upstream drops. Split the roles.

Overlapping routes use longest-prefix match, with no failover. If router A advertises 10.0.0.0/16 and router B advertises 10.0.0.0/24, traffic to 10.0.0.1 goes via B — and if B goes offline, Tailscale drops that traffic rather than falling back to A. For redundancy, have A advertise both prefixes.

Route approval and access control are separate mechanisms. Approval injects the route into routing tables; grants decide whether the packets are permitted. You need both.

Devices behind a subnet router don't count toward your plan's device limit.

Exit node

On the node:

shell
sudo tailscale set --advertise-exit-node
sudo tailscale up

The second command is required after set for this flag specifically. Linux exit nodes need the same IP forwarding sysctls as above.

Approve it: Machines → filter property:exit-node → Edit route settings → Use as exit node.

On the client:

shell
sudo tailscale set --exit-node=<exit-node-ip>
sudo tailscale set --exit-node=<exit-node-ip> --exit-node-allow-lan-access=true
sudo tailscale set --exit-node=                # stop

The CLI reference documents the flag as --exit-node=<ip|name> and also accepts --exit-node=auto:any, though the exit-node feature page only ever demonstrates the IP form. Get the IP from tailscale status or tailscale exit-node list. GUI clients have an Exit Node menu (macOS menu bar, Windows tray, in-app on mobile) with an "Allow local network access" toggle; the menu only appears when an exit node exists in the tailnet.

By default, using an exit node cuts off your own LAN — no local printer, no NAS by LAN IP. That's what --exit-node-allow-lan-access restores.

Exit nodes hijack DNS. "By default, when you configure a device to use an exit node, the device also uses the exit node as a DNS resolver for all domains, regardless of configuration of global and restricted (split DNS) nameservers." Override per-nameserver: DNS page → ⋯ next to the nameserver → Edit nameserver → Use with exit node.

The ACL mistake everyone makes: on a default policy, exit nodes just work. Once you write your own policy, the destination must be autogroup:internet. Naming the exit node device as dst only permits connections to that device (SSH etc.) — it does not permit using it as a gateway.

Platform caveats: requires v1.20+ on both ends. Android, macOS and Windows exit nodes use userspace routing — Android is slow and eats battery, macOS and Windows must be prevented from sleeping. Windows exit nodes spend a system thread per in-flight DNS query, which degrades under load.

07Working manual

Moving files and sharing services

Taildrop

Send: macOS right-click → Share → Tailscale. Windows right-click → "Send with Tailscale...". iOS/Android Share sheet → Tailscale. Linux:

shell
tailscale file cp ./my-file.txt my-phone:

Note the trailing colon on the target.

Receive: macOS ~/Downloads, Windows C:\Users\<username>\Downloads, mobile via notification. Linux has no automatic drop directory — files sit in an inbox until you pull them:

shell
sudo tailscale file get .

sudo because tailscaled runs as root and root receives the files. Useful flags: --wait, --loop, --conflict=skip|overwrite|rename.

Limits: your own devices only — you cannot Taildrop to another user's device. Same tailnet. Tagged nodes cannot send or receive Taildrop at all (this catches people who tag their homelab server). ACLs do not block Taildrop between your own devices.

Sharing a device with someone else

Machines → device menu → Share → by email or invite link. Reusable links work up to 1,000 times; unused links expire in 30 days.

The recipient reaches only that device, by full FQDN, and only if they're an Owner/Admin/IT admin of their own tailnet. Shared machines are quarantined: they can answer incoming connections but cannot initiate any. They don't advertise subnets, and all tag information is stripped.

Bonus: each unique user who accepts a share raises the device limit on both accounts by two.

Serve — expose a local service to your tailnet

shell
tailscale serve --bg 3000                    # proxy localhost:3000, persist in background
tailscale serve --bg --set-path=/blog 3000   # mount at a path
tailscale serve /home/alice/blog/index.html  # serve a file
sudo tailscale serve /tmp/public             # serve a directory
tailscale serve text:"Hello, world!"
tailscale serve https+insecure://localhost:8443   # backend has a self-signed cert
tailscale serve --tcp=2222 tcp://localhost:22     # raw TCP forward
tailscale serve status
tailscale serve reset

Published at https://<device>.<tailnet>.ts.net with a real cert. Requires MagicDNS + HTTPS enabled (the CLI offers to enable it). Tailnet-only, and normal grants apply.

Funnel — expose it to the public internet

shell
tailscale funnel --bg 3000
tailscale funnel status
tailscale funnel reset

This is public. Anyone on the internet can reach it.

Requirements and limits:

  • Only ports 443, 8443, 10000.
  • Requires HTTPS certs enabled for the tailnet.
  • TLS only. Funnel relays don't decrypt your traffic.
  • Non-configurable bandwidth limits.
  • Public DNS can take up to 10 minutes to propagate.
  • A port can't be Serve and Funnel simultaneously — last command wins.
  • --bg makes it survive a reboot. Without it you re-run manually after every restart.

Needs this in the policy file (the CLI adds it for you when you enable Funnel interactively):

json
"nodeAttrs": [
  {
    "target": ["autogroup:member"],
    "attr":   ["funnel"],
  },
],
08Working manual

Tailscale SSH

Replaces SSH key management. Authentication uses the WireGuard keys that already exist, governed by your policy file. No authorized_keys, no key distribution.

Server side (Linux and macOS open-source build only, v1.24+):

shell
tailscale set --ssh
tailscale set --ssh=false

Client side: ssh device, ssh ubuntu@device, or tailscale ssh user@host, from any platform with a shell. (The macOS App Store build has no tailscale ssh subcommand — use plain ssh there.)

You need two things in the policy file: an access rule permitting port 22, and an ssh rule. Minimal working policy:

json
{
  "grants": [
    {
      "src": ["group:sre"],
      "dst": ["tag:prod"],
      "ip":  ["*"]
    }
  ],
  "groups": {
    "group:sre": ["alice@example.com", "bob@example.com"]
  },
  "ssh": [
    {
      "action": "accept",
      "dst":    ["tag:prod"],
      "src":    ["group:sre"],
      "users":  ["ubuntu", "root"]
    }
  ],
  "tagOwners": {
    "tag:prod": ["group:sre"]
  }
}

action is accept (allow) or check (require browser re-auth first). checkPeriod sets the re-auth window: minimum 1 minute, maximum 168 hours, default 12 hours, or "always". Check rules are evaluated before accept rules and the more restrictive wins.

users accepts literal usernames plus autogroup:nonroot (anything but root).

Caveats:

  • Port 22 only. Custom SSH ports are not supported.
  • Restarting tailscaled — including an upgrade — kills every live session.
  • Not available on Synology or QNAP.
  • A tagged device cannot SSH into a user-owned device. By design.
  • Removing it tailnet-wide means disabling --ssh on every host and deleting the ssh rules.

Security footgun from the default policy: the shipped ssh rule uses "dst": ["autogroup:self"] with "users": ["autogroup:nonroot", "root"]. If you widen that dst to a tag without also narrowing users, everyone matching src can log in as any non-root user on those machines.

09Working manual

Servers that stay up

Four mechanisms, and they interlock.

Key expiry — the #1 cause of "my homelab vanished"

Node keys expire after 180 days by default (the docs also phrase this as "6 months"). When a key expires, all connections to and from that device stop. Configurable to 1–180 days at Device management → Key Expiry — but a change only applies to devices logged in after the change.

Fix an expired device you can reach:

shell
sudo tailscale up --force-reauth

Do not run that over SSH or RDP without another way in — it can drop the connection you're using.

Fix one you can't reach: Machines → device ⋯ menu → Temporarily extend key. Buys you 30 minutes.

Prevent it entirely: Machines → device ⋯ menu → Disable Key Expiry. Available on all plans, and the documented recommendation for "trusted servers, subnet routers, or remote IoT devices that are hard to reach." Do this on every headless box the day you set it up. The tradeoff is real: a compromised key stays valid until you delete the device.

Tags

Tagging a device changes its identity from you to the tag. Tags and user ownership are mutually exclusive: "Applying a tag to a device previously authenticated with a user account removes the user account. Similarly, authenticating a device with a user account removes all tags."

The homelab payoff: a device tagged for the first time and authenticated has key expiry disabled by default.

Define the tag first — there is no tags section, only tagOwners:

json
{
  "tagOwners": {
    "tag:server": ["dave@example.com"],
    "tag:infrastructure": [],
  }
}

An empty list means only Owners, Admins and Network admins can assign it.

Then apply it:

shell
sudo tailscale login --advertise-tags=tag:server
sudo tailscale login --advertise-tags=tag:server,tag:development
sudo tailscale up --advertise-tags=tag:server --force-reauth

A device's identity is the union of its tags, not the intersection — you can't write a rule requiring both tag:prod and tag:database. Use a composite tag:prod-database. You cannot remove the last tag from a tagged device, and you can't remove tags via --advertise-tags on a device that joined with an auth key (generate a new key instead). CLI tagging works on Linux, macOS and Windows only. All plans include 50 tagged devices.

Two tag side effects to plan around: tagged devices can't use Taildrop, and tagged devices can't SSH into user-owned devices.

Auth keys

Register a device without a browser:

shell
sudo tailscale up --auth-key=tskey-abcdef1432341818

Note the hyphen: the documented spelling is --auth-key. Keys are case-sensitive. Generate at login.tailscale.com/admin/settings/keys.

Options: one-off (single use) or reusable; plus ephemeral (device auto-removes when it goes offline), pre-approved (skips device approval), and tags. Expiry is 1–90 days, defaulting to the 90-day maximum. No renewal — generate a new one.

Reusable keys are dangerous if stolen; the docs push you toward a key vault. And note: revoking a key does not deauthorize devices that already used it. Delete the device from the Machines page for that.

Auth key expiry and node key expiry are independent. A device registered by a now-expired auth key keeps working until its own node key expires.

Running unattended

Linux: nothing to do. "Tailscale runs as the system, and is available even when no users are logged in."

Windows: by default Tailscale runs as the logged-in user, so it disconnects at logout and on reboot. Fix it — tray icon → Preferences → Run unattended, or:

powershell
tailscale up --unattended=true

macOS: not supported. There is no run-as-system mode. Tracked at github.com/tailscale/tailscale/issues/987. If you need an always-on macOS node, expect to keep a user logged in.

10Working manual

Access control

New tailnets ship allow-all. Everything reaches everything. For a single-person homelab that's often the correct answer — the risk is a device compromise, not a colleague.

The default, verbatim:

json
{
  "acls": [
    {
      "action": "accept",
      "src": ["*"],
      "dst": ["*:*"]
    }
  ],
  "ssh": [
    {
      "action": "check",
      "src": ["autogroup:member"],
      "dst": ["autogroup:self"],
      "users": ["autogroup:nonroot", "root"]
    }
  ],
}

The trap: omitting the acls field is not deny-all. An empty or missing acls silently reapplies the default allow-all. Deny-all is an explicit empty array: {"acls": []}.

The file is HuJSON — trailing commas and // comments are legal.

grants is now the recommended form over acls. "Grants are the recommended method and offer more functionality. However, ACLs will always be supported." Grants have no action field; Tailscale is deny-by-default so a grant implies accept.

json
{
  "grants": [
    {
      "src": ["group:home"],
      "dst": ["tag:server"],
      "ip":  ["tcp:22", "tcp:443", "1000-2000"]
    }
  ]
}

Sections you'll actually touch:

KeyFor
grantsAccess rules (preferred)
aclsAccess rules (legacy, still supported)
sshWho can Tailscale-SSH where, as which user
tagOwnersDefine tags and who may assign them
groupsNamed user groups (must be group:-prefixed; groups can't nest)
hostsNamed aliases for IPs and CIDRs
nodeAttrsPer-device attributes — funnel, nextdns:<id>, randomize-client-port
autoApproversSkip manual route/exit-node approval
testsAssertions that run on every save and block a bad policy

Auto-approval, worth setting up once so you stop clicking Approve:

json
"autoApprovers": {
  "routes": {
    "192.0.2.0/24": ["group:engineering", "alice@example.com", "tag:foo"],
  },
  "exitNode": ["tag:bar"],
}

Auto-approvers are not retroactive. "Updating the tailnet policy file to add or modify auto-approvers does not retroactively approve existing unapproved routes." You must remove the route from the subnet router and advertise it again. Prefer a tag as the approver — a user-based approver stops working if that user is suspended or deleted.

Write tests if you edit policy more than once. They run on save and reject a file that breaks an assertion, which is a cheaper way to find out than losing remote access.

11Working manual

Troubleshooting

The order to work in

1. tailscale status

text
100.1.2.3   device-a  you@       linux     active; direct <ip-port>, tx 1116 rx 1124
100.4.5.6   device-b  you@       macOS     active; relay <relay-server>, tx 1351 rx 4262
100.7.8.9   device-c  you@       windows   idle; tx 1214 rx 50
100.0.1.2   device-d  you@       iOS       -

Columns: Tailscale IP, machine name, owner, OS, connection state. active means traffic is flowing now and names the path — direct, relay (with a DERP city code like nyc, fra, tok, syd), or peer-relay. idle means connected but quiet. - means never any traffic.

relay is not broken, just slower. - on a device you expect to reach usually means policy or an expired key.

2. tailscale ping <host> — more detail than regular ping, and it's the only ping that works in userspace networking mode. --until-direct stops as soon as a direct path forms, which tells you whether NAT traversal is succeeding at all.

3. tailscale netcheck

text
Report:
	* UDP: true
	* IPv4: yes, <ipv4-address>
	* IPv6: yes, <ipv6-address>
	* MappingVariesByDestIP: false
	* PortMapping:
	* Nearest DERP: Seattle
FieldRead it as
UDP: falseNo direct connections possible. Everything falls back to DERP. Usually a restrictive firewall.
MappingVariesByDestIP: trueHard NAT. Direct connections are unreliable.
PortMapping blankNo UPnP / NAT-PMP / PCP on your router. Direct connections still possible, just less likely.
HairPinning: falseYour router can't route LAN → its own public IP → LAN. Affects same-LAN peers.
Anything blankTailscale couldn't measure it.

4. tailscale whois <ip> — confirms which identity, tags and capabilities a peer actually has. A tagged device prints Tags: and no User: block. Fastest way to confirm a tag silently replaced a user identity.

5. tailscale bugreport — run it while the problem is happening. Prints an identifier for support. Shares no PII and does nothing unless you hand the ID over. --record brackets a reproduction and gives two IDs; share both.

Symptom → cause

SymptomLikely cause
Device vanished from the tailnetNode key expired (180-day default). sudo tailscale up --force-reauth, or Temporarily extend key from the console.
A flag you set yesterday is goneYou ran bare tailscale up. Flags don't persist across up. Use tailscale set.
Subnet router advertises, nothing routesRoutes not approved in the console, IP forwarding sysctls not set, or a Linux client missing --accept-routes.
Exit node visible but unusableCustom policy without autogroup:internet as dst. Naming the device isn't enough.
Exit node on, LAN goneExpected. Add --exit-node-allow-lan-access=true.
DNS wrong while on an exit nodeExit node is the resolver for all domains by default. Enable "Use with exit node" on the nameserver.
Everything relayed, never directUDP: false or MappingVariesByDestIP: true in netcheck. Open UDP 41641 outbound.
You "tightened" the policy and nothing changedYou removed acls instead of setting it to []. That's allow-all.
New route ignored after adding autoApproversNot retroactive. Re-advertise the route.
Taildrop won't reach the serverIt's tagged. Tagged nodes can't do Taildrop.
Can't SSH from server to laptopTagged device → user-owned device is blocked by design.
https://hostname fails, http://hostname worksBare-hostname HTTPS is impossible. Use the full .ts.net FQDN.
macOS nslookup can't see MagicDNS namesExpected. Use dscacheutil -q host -a name my-server.
Second VPN breaks everythingIP range conflict on 100.64.0.0/10, or a one-VPN-at-a-time OS limit.
Locked yourself out over SSH--force-reauth and tailscale down can sever your session. --accept-risk=lose-ssh acknowledges it; it doesn't prevent it.
12Working manual

Homelab setup checklist

  1. Sign in on your daily driver first. Choose the IdP deliberately — GitHub and Apple can't be migrated away from later.
  2. Confirm MagicDNS is on at admin/dns.
  3. Install on the server. curl -fsSL https://tailscale.com/install.sh | sh then sudo tailscale up.
  4. Define tag:server in tagOwners, then re-auth the server with --advertise-tags=tag:server. Key expiry is now disabled on it. Accept that Taildrop-to-server and server-initiated SSH stop working.
  5. On any always-on Windows box: tailscale up --unattended=true.
  6. Subnet router for the gear that can't run Tailscale: set the sysctls, tailscale set --advertise-routes=..., approve in console, tailscale set --accept-routes on Linux clients.
  7. Exit node if you want the traditional-VPN behavior: tailscale set --advertise-exit-node + tailscale up, approve, then select it per-client with LAN access on.
  8. Add autoApprovers so step 6 and 7 stop requiring console clicks — remembering it only applies to routes advertised after the policy change.
  9. Enable HTTPS certs only if no hostname is sensitive. Then tailscale serve --bg <port> for internal dashboards.
  10. Funnel only when you genuinely mean "the public internet." Ports 443/8443/10000, and use --bg so it survives a reboot.
  11. Generate a reusable, tagged, pre-approved auth key and store it somewhere real — that's your rebuild path.
  12. Leave the default allow-all policy until you have a reason. When you change it, add tests first.

Every command, flag, default and JSON snippet above was checked against the source pages in a second pass. Where the docs are ambiguous or silent, the text says so rather than guessing.

Sources: Tailscale docs · What is Tailscale · Glossary · IP addresses · Reserved IPs · Install Linux · Identity providers · CLI reference · MagicDNS · DNS in Tailscale · Tailnet name · HTTPS certificates · Subnet routers · Exit nodes · Taildrop · Sharing · Serve · Funnel · Tailscale SSH · Access control · Policy file syntax · ACL examples · Auth keys · Key expiry · Tags · Run unattended · Firewall ports FAQ · Other VPNs FAQ