ts-autoserve makes local services reachable from other machines without anyone asking for it each time. That is the feature, and it is also the whole risk. This document is the honest list.
Published ports are reachable by every device and user in your tailnet, and by anyone those users have shared a device with. Tailscale terminates TLS and applies your policy file; it does not add authentication to an app that has none.
The daemon never uses Funnel, so nothing is exposed to the public internet unless you configure Funnel yourself.
A laptop listens on more than dev servers. Some ports are catastrophic to share:
| Port | What it is | What sharing it means |
|---|---|---|
| 9222 | Chrome remote debugging | Full control of the browser: read cookies, sessions, any logged-in account. Excluded by default. |
| 4444 | WebDriver / Selenium | Drive the browser, same outcome. |
| 5900 | VNC | The desktop. |
| 6379, 27017, 5432, 3306 | Redis, Mongo, Postgres, MySQL | Usually no password on localhost. |
| 2375, 2376 | Docker API | Root on the host, in practice. |
| 8888 | Jupyter | Arbitrary code execution, if the token is weak or absent. |
This is why the default is an allowlist plus agent ancestry, not "every port".
mode: all removes that protection — use it only on a machine where you know
everything that listens, and keep exclude_ports current.
A dev server is not a hardened server. It typically has no authentication, wide
open CORS, source maps, verbose stack traces, an HMR websocket that accepts any
origin, and .env values inlined into the bundle. Several framework dev servers
have shipped arbitrary-file-read bugs, on the theory that only the developer can
reach them. Publishing one on the tailnet moves it into reach of every device you
own — including any that is lost, shared, or compromised.
"Tailnet only" means every device you own, every user in the tailnet, every device shared into it, and any tagged CI node. If that is broader than you want for a dev server, restrict it in the policy file with a grant that limits who can reach this node's ports, rather than relying on the daemon.
Tailscale Serve injects Tailscale-User-Login, Tailscale-User-Name and
Tailscale-User-Profile-Pic into proxied requests. Two consequences: your app
learns who is calling (fine), and an app that trusts those headers must be sure
they cannot be spoofed by a request that did not come through Serve. If the same
app also listens on localhost directly, anything local can set those headers.
mode: agent and mode: both publish whatever an agent-spawned process is
listening on, including ports you never chose. An agent that starts an inspector,
a debug bridge, or a database container gets that port published too. Ancestry
matching is a heuristic: it skips the listening process itself to avoid matching
daemons named after agents, but it cannot tell a dev server from anything else an
agent happens to open. exclude_ports is the backstop.
The daemon needs operator permission (tailscale set --operator=$USER), which
lets it read and write the node's entire serve configuration. It performs a
read-modify-write per change, so a tailscale serve command run by hand at the
same moment can lose. Run it as your own user, never as root.
Every published URL, port and process name is sent to whatever notifier you
configure. That reveals what you are working on to Telegram, or to whoever
operates your webhook endpoint. Keep the Telegram token in an environment
variable (token_env) rather than the config file, and keep the config file
readable only by you (chmod 600).
- Keep
modeatdevorboth; avoidall. - Review
exclude_portsfor your machine — add database, debugger and admin ports before the first run, and check-once -dry-runoutput. - Restrict who can reach the node's ports in your tailnet policy file.
- Do not run it on a machine that holds production credentials in a service that happens to listen on a dev port.
- Prefer
token_envfor the Telegram token;chmod 600the config. - Do not add Funnel to a published port without understanding that it makes the service public to the internet, unauthenticated.
Open a private security advisory on the repository, or email dev@joseli.to. Please do not open a public issue for a vulnerability.