Route tailssh's forwarding through the per-connection ssh-monitor, so
on unix forwarded sockets are dialed and bound by children with the
authenticated local user's credentials, never by root tailscaled.
- unix -L, direct-streamlocal: the lexical allowlist check stays in
tailscaled as coarse policy. The allowlist is home, /tmp, and
/run/user/<uid>. Relative and abstract paths are rejected. The dial
then runs as the user through the monitor. The EvalSymlinks
re-validation is dropped, and the kernel's DAC is the boundary. The
allowlist is capped. A home directory that cleans to "/" or is empty
would prefix-match every absolute path, and such an entry is dropped
instead. Paths at or beyond the sun_path capacity reject lexically
rather than dying at bind or dial with a generic EINVAL.
- unix -R, streamlocal-forward: the listener is created by a user-cred
child with umask-at-bind 0600 and unlink-if-owned. Closing it does
not unlink the socket, and the stale file on cancel is accepted.
- TCP -R, tcpip-forward: a tailssh-owned handler replaces gliderssh's
ForwardedTCPHandler. The bind goes through the monitor's listen op
with the local user's credentials, so privileged ports fail by
kernel construction for non-root-mapped users. Root-mapped users are
allowed, as in OpenSSH. Bind addresses are loopback-only unless
TS_SSH_FORWARD_GATEWAY_PORTS is set, the equivalent of sshd's
GatewayPorts.
- agent forwarding: the socket and its containing directory are
created by a user-cred child, a 0600 socket in a 0700 user-owned
directory, instead of a root-run MkdirTemp, Chmod, and Chown
sequence. Session-end cleanup is unchanged.
The monitor is spawned lazily on the first forwarding or agent need,
from the tailscaled binary, and closed when the SSH connection ends.
closeFwdMonitor latches a closed flag under the fwd lock, so a caller
racing the teardown is refused by fwdMonitor, or reads the current
state without spawning through fwdMonitorIfAny, instead of respawning
a monitor nobody ever closes.
plan9 keeps today's behavior through a build-tagged seam, fwdmonitor.go
and fwdplan9.go. There is no monitor on plan9. Unix forwarding is
rejected there, and reverse TCP forwarding keeps the gliderssh
handler with the mayReversePortForwardTo gate.
Tests cover conn-level callbacks against a real monitor and real
one-shot fwd children: allowlist rejections, user-owned 0600 sockets, a
foreign sticky-dir socket not unlinked, loopback, gateway-ports, and
privileged-port TCP binds, agent directory hygiene and cleanup, and
TS_SSH_DISABLE_FORWARDING coverage, plus refused respawn after
teardown. The unix-fwd root-dial regressions cover a static
in-allowlist symlink to a root-owned 0600 canary, which must fail
permission denied as the bound user, and a dir-swap race, which must
never serve the protected canary.
Updates tailscale/corp#50380
Signed-off-by: James Tucker <james@tailscale.com>
The agent forwarding socket's temp directory was left root-owned and
chmod 0755, letting every local user reach other users' agent sockets.
Chown the directory to the session user and keep it 0700 (the socket is
already 0600 and chowned first), like sshd's agent forwarding, and
remove the directory when the session ends instead of only closing the
listener.
Updates tailscale/corp#50380
Signed-off-by: James Tucker <james@tailscale.com>