3 Commits
Author SHA1 Message Date
James Tucker b101cb8bef ssh/tailssh: keep sessions when a prompt-free rule expires
Check mode grants recently approved users a prompt-free Accept rule
with a short RuleExpires. When that rule expired, the policy recheck
matched the base HoldAndDelegate rule instead. The recheck read that as
a policy edit and closed the connection. Live sessions died when the
prompt-free window lapsed.

Remember the rule that matched at authentication. If it has expired and
a hold rule matches instead, keep the connection. Only new connections
re-authenticate.

The expiry timer now fires at least 1ms past a lapsed deadline. A
zero-delay refire lands at the deadline, where the rule is not yet
expired, and re-arms. The real clock moves on between refires; a stopped
clock, such as a synctest bubble, does not.

The fallback test runs in a synctest bubble, so the expiry cannot race
the handshake on a slow runner. A bubble cannot hold child processes:
the test opens a session channel but runs no command, and the empty PATH
keeps the conn lifecycle free of subprocesses. The session-kill side of
expiry stays pinned on the real clock.

A policy edit that swaps a still-valid accept rule for a hold rule still
closes the connection. Recorder, identity, and accept-env checks still
run on the fallback path.

Updates tailscale/corp#50380

Change-Id: I46fa7d42142e90d00843744427c91ba29d68a467
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-28 14:47:00 -07:00
James Tucker 87f3b61f31 ssh/tailssh: dial TCP forwards as the bound user
Route direct-tcpip channels, used by TCP -L and SOCKS -D forwards, through the per-connection monitor after the existing policy checks. Resolve names and create/connect TCP sockets in a child running with the authenticated local user credentials instead of the root daemon.

Hand the connected TCP descriptor back through the existing acknowledged fd protocol. TCP socket ownership survives the handoff, so UID-based firewall policy sees the mapped user. TCP has no per-message SCM_CREDENTIALS and needs no persistent relay: the child exits after handing off the socket. Bound resolution/dialing by the monitor forwarding deadline.

Share the channel-close-aware forwarding copy loop with Unix forwards, retaining bidirectional half-closes and bounded draining on full close. Plan 9 retains its existing in-process forwarding path.

Add root-run IPv4/IPv6 socket-owner regression tests, hostname and half-close coverage, policy/disable gates, invalid/refused dial handling, and a pin that the child is reaped while its socket remains usable. Update the TCP policy revocation fixture for child spawning and synchronize its assertion on transport shutdown.

Updates tailscale/corp#50380

Change-Id: I89159fd31e75c22446096587471afcd2bb4e78d4
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 13:10:45 -07:00
James Tucker d75f04c927 ssh/tailssh: enforce SSH policy at the connection level
Re-evaluate authenticated connections on policy updates and at session
and forwarding gates. Close connections whose grants are revoked or
narrowed, including forwarding permissions, user identity, duration,
AcceptEnv, and effective recording requirements. Recheck rule-derived
AcceptEnv and recording configuration for delegated connections too.
Compare recorder lists with duplicate counts preserved so duplicate
entries cannot hide a change of recording destination.

Treat recording-failure changes in either direction as material,
including fail-open to fail-closed transitions. An inconclusive user
lookup retains the previous identity but does not skip other policy
checks, including for `"*": "="` mappings. An absent account or a changed
username, UID, or primary GID revokes the connection.

Preserve explicit getent failures when the os/user fallback cannot find
the account, instead of treating every lookup failure as account deletion.
Numeric UID mappings use LookupId in the fallback, not Lookup by name.
Known limitation: NSS can report a directory-only account as not found
(getent exit status 2) during an sssd/LDAP outage. If the fallback also
reports the account missing, this is indistinguishable from deletion and
still revokes the connection. This availability tradeoff remains accepted
for now; explicit command failures, timeouts, and malformed output do not
by themselves establish that an account was deleted.

Reject forwarding requests using the latched Allow bit before doing a
policy recheck. Forwarding gates still re-evaluate policy, but use the
latched account identity rather than spawning getent for every forwarded
connection (including each SSH -D request). Account identity is rechecked
at authentication completion, policy updates, rule expiry, and session
gates.

Re-arm rule-expiry timers under the policy recheck lock and prevent
re-arming after teardown. Add connection-level login grace, extending
it during delegation and stopping it only on SSH authentication
success, not on unsigned key queries or partial authentication.
Close the raw transport and cancel delegated authentication on teardown.

Add regression tests for these transitions, wait for the auth-completion
recheck before test policy flips, and release stalled recorder handlers
even on test failure.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 08:48:36 -07:00