mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-29 22:49:17 -04:00
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>
61 lines
2.0 KiB
Go
61 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux || darwin
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// TestSSHSFTPOnRevokedConn pins the ordering between the
|
|
// recheck-on-new-session gate and the sftp recording path. A conn whose
|
|
// policy was revoked mid-conn refuses a new sftp session channel with
|
|
// the revocation notice at exit 255 and never reaches recording setup.
|
|
func TestSSHSFTPOnRevokedConn(t *testing.T) {
|
|
lu := sessionTestUser(t)
|
|
// The standing action carries recorders, none of them reachable, so
|
|
// recording setup fails for any session that reaches it. The gate
|
|
// under test must refuse the session before that.
|
|
p := &policyFlipper{localState: &localState{sshEnabled: true, varRoot: t.TempDir()},
|
|
rule: newPolicyRule(&tailcfg.SSHAction{
|
|
Accept: true,
|
|
Recorders: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:1")},
|
|
OnRecordingFailure: &tailcfg.SSHRecorderFailureAction{
|
|
RejectSessionWithMessage: "session rejected",
|
|
},
|
|
}, lu)}
|
|
_, client, _ := dialPolicyConn(t, p)
|
|
|
|
// Revoke with the rule flipping to Reject and no OnPolicyChange kick.
|
|
// The conn stays open. The recheck-on-new-session gate is what
|
|
// refuses the next session channel.
|
|
p.setRule(newPolicyRule(&tailcfg.SSHAction{Reject: true}, lu))
|
|
|
|
sess, err := client.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("new session channel: %v", err)
|
|
}
|
|
sess.Close()
|
|
|
|
status, out := sftpSubsystemSession(t, client)
|
|
if status != 255 {
|
|
t.Errorf("exit status = %d; want 255 (access revoked). output %q", status, out)
|
|
}
|
|
if !strings.Contains(out, "access revoked") {
|
|
t.Errorf("refusal output = %q; want access-revoked notice", out)
|
|
}
|
|
if strings.Contains(out, "session rejected") {
|
|
t.Errorf("refusal output %q: recording policy ran before the revocation gate", out)
|
|
}
|
|
eventually(t, 10*time.Second, "connection not closed after revocation", func() bool {
|
|
return sessionFails(client)
|
|
})
|
|
}
|