|
|
|
|
@ -75,8 +75,8 @@ func init() { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// HandleSSHConn handles a Tailscale SSH connection from c.
|
|
|
|
|
func (s *server) HandleSSHConn(c net.Conn) error { |
|
|
|
|
ss, err := s.newSSHServer() |
|
|
|
|
func (srv *server) HandleSSHConn(c net.Conn) error { |
|
|
|
|
ss, err := srv.newSSHServer() |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
@ -88,6 +88,16 @@ func (s *server) HandleSSHConn(c net.Conn) error { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// OnPolicyChange terminates any active sessions that no longer match
|
|
|
|
|
// the SSH access policy.
|
|
|
|
|
func (srv *server) OnPolicyChange() { |
|
|
|
|
srv.mu.Lock() |
|
|
|
|
defer srv.mu.Unlock() |
|
|
|
|
for _, s := range srv.activeSessionByH { |
|
|
|
|
go s.checkStillValid() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (srv *server) newSSHServer() (*ssh.Server, error) { |
|
|
|
|
ss := &ssh.Server{ |
|
|
|
|
Handler: srv.handleSSH, |
|
|
|
|
@ -102,13 +112,13 @@ func (srv *server) newSSHServer() (*ssh.Server, error) { |
|
|
|
|
Version: "SSH-2.0-Tailscale", |
|
|
|
|
LocalPortForwardingCallback: srv.mayForwardLocalPortTo, |
|
|
|
|
NoClientAuthCallback: func(m gossh.ConnMetadata) (*gossh.Permissions, error) { |
|
|
|
|
if srv.requiresPubKey(m.User(), m.LocalAddr(), m.RemoteAddr()) { |
|
|
|
|
if srv.requiresPubKey(m.User(), toIPPort(m.LocalAddr()), toIPPort(m.RemoteAddr())) { |
|
|
|
|
return nil, errors.New("public key required") // any non-nil error will do
|
|
|
|
|
} |
|
|
|
|
return nil, nil |
|
|
|
|
}, |
|
|
|
|
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool { |
|
|
|
|
if srv.acceptPubKey(ctx.User(), ctx.LocalAddr(), ctx.RemoteAddr(), key) { |
|
|
|
|
if srv.acceptPubKey(ctx.User(), toIPPort(ctx.LocalAddr()), toIPPort(ctx.RemoteAddr()), key) { |
|
|
|
|
srv.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(key))) |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
@ -149,7 +159,7 @@ func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string |
|
|
|
|
// requiresPubKey reports whether the SSH server, during the auth negotiation
|
|
|
|
|
// phase, should requires that the client send an SSH public key. (or, more
|
|
|
|
|
// specifically, that "none" auth isn't acceptable)
|
|
|
|
|
func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr net.Addr) bool { |
|
|
|
|
func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort) bool { |
|
|
|
|
pol, ok := srv.sshPolicy() |
|
|
|
|
if !ok { |
|
|
|
|
return false |
|
|
|
|
@ -184,7 +194,7 @@ func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr net.Addr |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (srv *server) acceptPubKey(sshUser string, localAddr, remoteAddr net.Addr, pubKey ssh.PublicKey) bool { |
|
|
|
|
func (srv *server) acceptPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) bool { |
|
|
|
|
a, _, _, err := srv.evaluatePolicy(sshUser, localAddr, remoteAddr, pubKey) |
|
|
|
|
if err != nil { |
|
|
|
|
return false |
|
|
|
|
@ -220,19 +230,16 @@ func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) { |
|
|
|
|
return nil, false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func asTailscaleIPPort(a net.Addr) (netaddr.IPPort, error) { |
|
|
|
|
func toIPPort(a net.Addr) (ipp netaddr.IPPort) { |
|
|
|
|
ta, ok := a.(*net.TCPAddr) |
|
|
|
|
if !ok { |
|
|
|
|
return netaddr.IPPort{}, fmt.Errorf("non-TCP addr %T %v", a, a) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
tanetaddr, ok := netaddr.FromStdIP(ta.IP) |
|
|
|
|
if !ok { |
|
|
|
|
return netaddr.IPPort{}, fmt.Errorf("unparseable addr %v", ta.IP) |
|
|
|
|
} |
|
|
|
|
if !tsaddr.IsTailscaleIP(tanetaddr) { |
|
|
|
|
return netaddr.IPPort{}, fmt.Errorf("non-Tailscale addr %v", ta.IP) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
return netaddr.IPPortFrom(tanetaddr, uint16(ta.Port)), nil |
|
|
|
|
return netaddr.IPPortFrom(tanetaddr, uint16(ta.Port)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// evaluatePolicy returns the SSHAction, sshConnInfo and localUser after
|
|
|
|
|
@ -241,29 +248,27 @@ func asTailscaleIPPort(a net.Addr) (netaddr.IPPort, error) { |
|
|
|
|
//
|
|
|
|
|
// The return sshConnInfo will be non-nil, even on some errors, if the
|
|
|
|
|
// evaluation made it far enough to resolve the remoteAddr to a Tailscale IP.
|
|
|
|
|
func (srv *server) evaluatePolicy(sshUser string, localAddr, remoteAddr net.Addr, pubKey ssh.PublicKey) (_ *tailcfg.SSHAction, _ *sshConnInfo, localUser string, _ error) { |
|
|
|
|
func (srv *server) evaluatePolicy(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) (_ *tailcfg.SSHAction, _ *sshConnInfo, localUser string, _ error) { |
|
|
|
|
pol, ok := srv.sshPolicy() |
|
|
|
|
if !ok { |
|
|
|
|
return nil, nil, "", fmt.Errorf("tsshd: rejecting connection; no SSH policy") |
|
|
|
|
return nil, nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy") |
|
|
|
|
} |
|
|
|
|
srcIPP, err := asTailscaleIPPort(remoteAddr) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, nil, "", fmt.Errorf("tsshd: rejecting: %w", err) |
|
|
|
|
if !tsaddr.IsTailscaleIP(remoteAddr.IP()) { |
|
|
|
|
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", remoteAddr) |
|
|
|
|
} |
|
|
|
|
dstIPP, err := asTailscaleIPPort(localAddr) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, nil, "", err |
|
|
|
|
if !tsaddr.IsTailscaleIP(localAddr.IP()) { |
|
|
|
|
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", localAddr) |
|
|
|
|
} |
|
|
|
|
node, uprof, ok := srv.lb.WhoIs(srcIPP) |
|
|
|
|
node, uprof, ok := srv.lb.WhoIs(remoteAddr) |
|
|
|
|
if !ok { |
|
|
|
|
return nil, nil, "", fmt.Errorf("unknown Tailscale identity from src %v", srcIPP) |
|
|
|
|
return nil, nil, "", fmt.Errorf("unknown Tailscale identity from src %v", remoteAddr) |
|
|
|
|
} |
|
|
|
|
ci := &sshConnInfo{ |
|
|
|
|
now: time.Now(), |
|
|
|
|
fetchPublicKeysURL: srv.fetchPublicKeysURL, |
|
|
|
|
sshUser: sshUser, |
|
|
|
|
src: srcIPP, |
|
|
|
|
dst: dstIPP, |
|
|
|
|
src: remoteAddr, |
|
|
|
|
dst: localAddr, |
|
|
|
|
node: node, |
|
|
|
|
uprof: &uprof, |
|
|
|
|
pubKey: pubKey, |
|
|
|
|
@ -304,7 +309,7 @@ func (srv *server) handleSSH(s ssh.Session) { |
|
|
|
|
logf := srv.logf |
|
|
|
|
|
|
|
|
|
sshUser := s.User() |
|
|
|
|
action, ci, localUser, err := srv.evaluatePolicy(sshUser, s.LocalAddr(), s.RemoteAddr(), s.PublicKey()) |
|
|
|
|
action, ci, localUser, err := srv.evaluatePolicy(sshUser, toIPPort(s.LocalAddr()), toIPPort(s.RemoteAddr()), s.PublicKey()) |
|
|
|
|
if err != nil { |
|
|
|
|
logf(err.Error()) |
|
|
|
|
s.Exit(1) |
|
|
|
|
@ -433,6 +438,21 @@ func (srv *server) newSSHSession(s ssh.Session, ci *sshConnInfo, lu *user.User) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// checkStillValid checks that the session is still valid per the latest SSHPolicy.
|
|
|
|
|
// If not, it terminates the session.
|
|
|
|
|
func (ss *sshSession) checkStillValid() { |
|
|
|
|
ci := ss.connInfo |
|
|
|
|
a, _, _, err := ss.srv.evaluatePolicy(ci.sshUser, ci.src, ci.dst, ci.pubKey) |
|
|
|
|
if err == nil && (a.Accept || a.HoldAndDelegate != "") { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
ss.logf("session no longer valid per new SSH policy; closing") |
|
|
|
|
ss.ctx.CloseWithError(userVisibleError{ |
|
|
|
|
fmt.Sprintf("Access revoked.\n"), |
|
|
|
|
context.Canceled, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) { |
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute) |
|
|
|
|
defer cancel() |
|
|
|
|
|