Report vulnerabilities privately through GitHub's private vulnerability reporting on matchory/tslink. Do not open a public issue, pull request or discussion for them.
Include what is affected, how to reproduce it, and the impact you expect. We will confirm receipt, keep you informed while we work on a fix, and credit you in the advisory unless you prefer otherwise.
Vulnerabilities in Tailscale itself go to Tailscale, and those in Docker to the Moby project.
Fixes go into the latest release. Older releases are not patched.
tslink makes the guarantees below, provided the environment properties after
them hold. Each guarantee names who it defends against and which properties
it relies on. Tests that verify a guarantee carry its ID (Guards: G1); a
check in the test suite fails if a guarantee listed here has none.
Attackers: a process in a container on a tslink network, as root inside it, with Docker's default capabilities. Assumes: E1, E2, E3, E5.
A container cannot reach the tailnet through the host's own Tailscale, by
sockets or by raw frames, through tslink's veth or through Docker's gateway.
This holds with the plugin setting TSLINK_ISOLATE_HOST_TAILNET at its
default, true.
Attackers: a process in any container on the host, on a tslink network or
any other, as root inside it, with Docker's default capabilities; a host on
the LAN that routes tslink's veth range through the Docker host.
Assumes: E1, E2, E3, and of E5 the mangle table and the plugin's privileges:
this isolation is on whatever TSLINK_ISOLATE_HOST_TAILNET says.
Each tslink container's tailscaled has a veth pair in 10.200.0.0/16, and
the host forwards between them. Nobody reaches a tslink container's veth
address through that, by sockets or by raw frames, past Docker's network
isolation and the tailnet's ACLs, whatever interface the traffic comes from,
a bridge with its own name included. The host forwards to tslink's veths only
replies on connections their container opened, and WireGuard (UDP 41641)
from other tslink veths, so colocated nodes keep their direct path. That UDP
port is open to tslink's other containers: WireGuard drops what is not from a
peer.
Attackers: a process in a container on a tslink network, as root inside it, with Docker's default capabilities; the author of a stack file that passed review. Assumes: E1, E2, E3.
The plugin runs as root on the host. Labels (tslink.hostname,
tslink.service, tslink.serve.<port>,
tslink.service.<name>.serve.<port>), network options, container and stack
names and the container's DNS settings do not make it act on anything but
the container they belong to and its own state:
- A Service name is a valid, lower-case Tailscale Service name, and a port is a number.
- A serve value forwards only to a port on the container's loopback, under its own Service or the node's own name.
- A hostname or stack name names a state directory inside the plugin's data directory only (see G6).
- tailscaled's
resolv.confholds only nameserver lines with an address and at most one search and one options line.
The plugin configures tailscaled through its LocalAPI with typed values,
never through the tailscale CLI, and runs fixed binaries with argument
arrays. Its one shell script, which starts tailscaled, takes values only as
arguments.
Attackers: the author of a stack file that passed review. Assumes: E1, E2, E3, E4, E7, E8.
A network that belongs to a stack gives tailnet nodes only to that stack's
tasks. With the cluster credential, a stack's nodes get only the tag
tag:<stack>, with the plugin setting TSLINK_TAG_SCOPE at its default,
exact; prefix also allows tag:<stack>-*, which lets a stack claim a
longer-named stack's base tag. Networks outside a stack cannot use the
cluster credential. The control server is the operator's choice, a plugin
setting; a stack network cannot name one. An http control server must be
an IP address: tailscaled resolves names with the container's DNS servers,
which a stack sets, and plain http does not authenticate the server.
A stack can label any Service name; whether control makes its nodes backends depends on the tailnet policy's auto-approvers (E8).
Each stack's certificates and certificate leases live in a directory of its own in the shared certificate directory, so naming another stack's Service does not delay or block that stack's certificates.
Attackers: a process in a container on a tslink network; a tailnet peer; the control server. Assumes: E2, E3.
Auth keys and OAuth client secrets do not appear in these places:
- the plugin log and status files;
- the command line and environment of the plugin and of tailscaled, except
that the plugin setting
TS_AUTHKEYis in the plugin's environment; - tailscaled's state and log, and the other files in the plugin's data
directory, but for
oauth-client.secret, which holds the cluster credential; - the containers;
- the output of
tslink diag.
tslink hands a key to tailscaled only through its LocalAPI. An OAuth client
secret goes only to the Tailscale API (api.tailscale.com, or the server
its baseURL attribute names), which mints a single-use auth key from it;
only that key goes to the control server.
Persons with access to the Docker API can read them in network options and plugin settings. See "Scope".
Attackers: a process in a container on a tslink network, as root inside it, with Docker's default capabilities; the author of a stack file that passed review. Assumes: E1, E2, E3.
Each node's Tailscale state, with its private keys, and its tailscaled's
LocalAPI are reachable only by the plugin and root on the host. tailscaled
shares only the network namespace with its container: it runs in a mount
namespace of its own, and its LocalAPI is a Unix socket at a path in the
plugin's data directory, which the container does not see. State
directories are by-hostname/<hostname>/, or by-stack/<stack>/<hostname>/
for stack tasks, in the plugin's data directory, created with mode 0700. A
hostname or stack name names no other path, and each state directory serves
one endpoint at a time. A certificate's files are named after a validated DNS
name, so neither a Service name nor the tailnet's DNS suffix from control
names a file outside the certificate directory.
| ID | Property | Ensured by | tslink diag --preflight |
|---|---|---|---|
| E1 | Containers on tslink networks, and the other containers of their stacks, are not privileged and have neither NET_ADMIN nor SYS_ADMIN, nor namespaced sysctls that enable forwarding, nor unconfined seccomp or AppArmor |
stack review, CI policy | checks privileges, capabilities, forwarding sysctls and unconfined seccomp or AppArmor |
| E2 | No container on a tslink network, nor another container of its stack, bind-mounts Docker's socket, /run/netns, Docker's data root (/var/lib/docker by default), the plugin's data directory (/var/lib/docker-plugins/tailscale, or wherever it was moved), the shared certificate directory, or a directory containing them, or shares the host's PID namespace |
stack review, CI policy | checks, including the shared certificate directory (each tslink plugin's own shared.source setting, the host path), but not a moved plugin data directory |
| E3 | Only operators have Docker API access and root on hosts | operator | does not check |
| E4 | Stacks are deployed with docker stack deploy, which sets com.docker.stack.namespace |
operator, CI | lists Swarm tasks outside stacks |
| E5 | The host supports the iptables mangle table for IPv4 and IPv6, the plugin runs with the privileges of its config.json, and its setting TSLINK_ISOLATE_HOST_TAILNET is at its default, true |
operator | checks the setting, and that tslink's isolation chains lead FORWARD and INPUT and drop traffic; not the plugin's privileges |
| E6 | The plugin is installed from a release image pinned by digest | operator | checks the digest pin |
| E7 | The shared certificate directory is owned by root, mode 0700 or stricter, and reachable only by the cluster's hosts | operator | checks owner and mode, with --shared-dir |
| E8 | The cluster credential owns only stack-prefixed tags, stack names follow tslink's rules, the plugin setting TSLINK_TAG_SCOPE is at its default, exact, and the policy auto-approves each Service only for the tag of the stack that owns it |
operator, tailnet policy | checks the tag scope setting |
| E9 | The identity of tslink's nodes depends on the control server, unless Tailnet Lock is on, which tslink is not tested with (Limitations) | operator | reports whether it is on |
Run tslink diag --preflight on each host, and on each node of a Swarm. It
checks only the properties that it can see on the host where it runs.
tslink diag shows how.
tslink is not tested with Tailnet Lock (E9). It is expected to work only with
a reusable auth key pre-signed with tailscale lock sign, not with an OAuth
client secret or the cluster credential, because tslink makes auth keys from
them that are not signed. See
Limitations.
The range of TSLINK_WIREGUARD_PORTS is reserved: a host service listening
in it gets no unsolicited UDP.
tslink is privileged by design. Think of this when you assess the impact of a finding:
- The plugin runs as root on the host with host networking,
CAP_NET_ADMIN,CAP_SYS_ADMIN,/dev/net/tun, the Docker socket, the host's/runand/var/lib/docker-plugins/tailscale. Docker mounts the host's whole/var/libinto the plugin, read-write; the plugin uses it only as it starts, to create/var/lib/docker-plugins/tailscaleand mount that at/data, and unmounts it before it opens its socket. Thus it serves no request while it sees the host's/var/lib, and refuses to start if it cannot unmount it. It enters container network namespaces and changes interfaces, routes and firewall rules. A flaw that lets a container or a tailnet peer influence what the plugin does on the host is in scope, and so is a way to make it keep the host's/var/libpast its start. - Credentials are visible to persons with access to the Docker API.
docker network inspectshows the network optiontslink.authkey, anddocker plugin inspectshows the plugin settingTS_AUTHKEY. On a Swarm, the Raft store of the managers keeps them. Access to the Docker API is root on the host, so this is expected and not a vulnerability. See Where credentials are visible. A credential that leaks to another place is in scope, for example to a log, the process list of the host or the application container. - Isolation between containers and stacks is in scope. Examples:
- A container reaches the tailnet through the Tailscale of the host or of another container.
- A task gets tags or an identity that its network does not give it.
- A stack uses the network or the state of another stack.
- State on disk:
/var/lib/docker-plugins/tailscaleholds each node's Tailscale state (its private keys), andoauth-client.secret, the cluster credential. Anyone who can read it can impersonate those nodes, or register nodes with the tags of every stack. The shared certificate directory holds every Service's TLS private key and the ACME account key: anyone who can read it can impersonate those Services (E7).