mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 05:07:33 +00:00
ssh/tailssh: accept passwords and public keys
Some clients don't request 'none' authentication. Instead, they immediately supply a password or public key. This change allows them to do so, but ignores the supplied credentials and authenticates using Tailscale instead. Updates #14922 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:

committed by
Percy Wegmann

parent
f2f7fd12eb
commit
db231107a2
@@ -51,6 +51,11 @@ var (
|
||||
sshDisableSFTP = envknob.RegisterBool("TS_SSH_DISABLE_SFTP")
|
||||
sshDisableForwarding = envknob.RegisterBool("TS_SSH_DISABLE_FORWARDING")
|
||||
sshDisablePTY = envknob.RegisterBool("TS_SSH_DISABLE_PTY")
|
||||
|
||||
// errTerminal is an empty gossh.PartialSuccessError (with no 'Next'
|
||||
// authentication methods that may proceed), which results in the SSH
|
||||
// server immediately disconnecting the client.
|
||||
errTerminal = &gossh.PartialSuccessError{}
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -230,8 +235,8 @@ type conn struct {
|
||||
finalAction *tailcfg.SSHAction // set by clientAuth
|
||||
|
||||
info *sshConnInfo // set by setInfo
|
||||
localUser *userMeta // set by doPolicyAuth
|
||||
userGroupIDs []string // set by doPolicyAuth
|
||||
localUser *userMeta // set by clientAuth
|
||||
userGroupIDs []string // set by clientAuth
|
||||
acceptEnv []string
|
||||
|
||||
// mu protects the following fields.
|
||||
@@ -255,46 +260,73 @@ func (c *conn) vlogf(format string, args ...any) {
|
||||
}
|
||||
|
||||
// errDenied is returned by auth callbacks when a connection is denied by the
|
||||
// policy. It returns a gossh.BannerError to make sure the message gets
|
||||
// displayed as an auth banner.
|
||||
func errDenied(message string) error {
|
||||
// policy. It writes the message to an auth banner and then returns an empty
|
||||
// gossh.PartialSuccessError in order to stop processing authentication
|
||||
// attempts and immediately disconnect the client.
|
||||
func (c *conn) errDenied(message string) error {
|
||||
if message == "" {
|
||||
message = "tailscale: access denied"
|
||||
}
|
||||
return &gossh.BannerError{
|
||||
Message: message,
|
||||
if err := c.spac.SendAuthBanner(message); err != nil {
|
||||
c.logf("failed to send auth banner: %s", err)
|
||||
}
|
||||
return errTerminal
|
||||
}
|
||||
|
||||
// bannerError creates a gossh.BannerError that will result in the given
|
||||
// message being displayed to the client. If err != nil, this also logs
|
||||
// message:error. The contents of err is not leaked to clients in the banner.
|
||||
func (c *conn) bannerError(message string, err error) error {
|
||||
// errBanner writes the given message to an auth banner and then returns an
|
||||
// empty gossh.PartialSuccessError in order to stop processing authentication
|
||||
// attempts and immediately disconnect the client. The contents of err is not
|
||||
// leaked in the auth banner, but it is logged to the server's log.
|
||||
func (c *conn) errBanner(message string, err error) error {
|
||||
if err != nil {
|
||||
c.logf("%s: %s", message, err)
|
||||
}
|
||||
return &gossh.BannerError{
|
||||
Err: err,
|
||||
Message: fmt.Sprintf("tailscale: %s", message),
|
||||
if err := c.spac.SendAuthBanner("tailscale: " + message); err != nil {
|
||||
c.logf("failed to send auth banner: %s", err)
|
||||
}
|
||||
return errTerminal
|
||||
}
|
||||
|
||||
// errUnexpected is returned by auth callbacks that encounter an unexpected
|
||||
// error, such as being unable to send an auth banner. It sends an empty
|
||||
// gossh.PartialSuccessError to tell gossh.Server to stop processing
|
||||
// authentication attempts and instead disconnect immediately.
|
||||
func (c *conn) errUnexpected(err error) error {
|
||||
c.logf("terminal error: %s", err)
|
||||
return errTerminal
|
||||
}
|
||||
|
||||
// clientAuth is responsible for performing client authentication.
|
||||
//
|
||||
// If policy evaluation fails, it returns an error.
|
||||
// If access is denied, it returns an error.
|
||||
func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
// If access is denied, it returns an error. This must always be an empty
|
||||
// gossh.PartialSuccessError to prevent further authentication methods from
|
||||
// being tried.
|
||||
func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retErr error) {
|
||||
defer func() {
|
||||
if pse, ok := retErr.(*gossh.PartialSuccessError); ok {
|
||||
if pse.Next.GSSAPIWithMICConfig != nil ||
|
||||
pse.Next.KeyboardInteractiveCallback != nil ||
|
||||
pse.Next.PasswordCallback != nil ||
|
||||
pse.Next.PublicKeyCallback != nil {
|
||||
panic("clientAuth attempted to return a non-empty PartialSuccessError")
|
||||
}
|
||||
} else if retErr != nil {
|
||||
panic(fmt.Sprintf("clientAuth attempted to return a non-PartialSuccessError error of type: %t", retErr))
|
||||
}
|
||||
}()
|
||||
|
||||
if c.insecureSkipTailscaleAuth {
|
||||
return &gossh.Permissions{}, nil
|
||||
}
|
||||
|
||||
if err := c.setInfo(cm); err != nil {
|
||||
return nil, c.bannerError("failed to get connection info", err)
|
||||
return nil, c.errBanner("failed to get connection info", err)
|
||||
}
|
||||
|
||||
action, localUser, acceptEnv, err := c.evaluatePolicy()
|
||||
if err != nil {
|
||||
return nil, c.bannerError("failed to evaluate SSH policy", err)
|
||||
return nil, c.errBanner("failed to evaluate SSH policy", err)
|
||||
}
|
||||
|
||||
c.action0 = action
|
||||
@@ -304,11 +336,11 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
// hold and delegate URL (if necessary).
|
||||
lu, err := userLookup(localUser)
|
||||
if err != nil {
|
||||
return nil, c.bannerError(fmt.Sprintf("failed to look up local user %q ", localUser), err)
|
||||
return nil, c.errBanner(fmt.Sprintf("failed to look up local user %q ", localUser), err)
|
||||
}
|
||||
gids, err := lu.GroupIds()
|
||||
if err != nil {
|
||||
return nil, c.bannerError("failed to look up local user's group IDs", err)
|
||||
return nil, c.errBanner("failed to look up local user's group IDs", err)
|
||||
}
|
||||
c.userGroupIDs = gids
|
||||
c.localUser = lu
|
||||
@@ -321,7 +353,7 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
metricTerminalAccept.Add(1)
|
||||
if action.Message != "" {
|
||||
if err := c.spac.SendAuthBanner(action.Message); err != nil {
|
||||
return nil, fmt.Errorf("error sending auth welcome message: %w", err)
|
||||
return nil, c.errUnexpected(fmt.Errorf("error sending auth welcome message: %w", err))
|
||||
}
|
||||
}
|
||||
c.finalAction = action
|
||||
@@ -329,11 +361,11 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
case action.Reject:
|
||||
metricTerminalReject.Add(1)
|
||||
c.finalAction = action
|
||||
return nil, errDenied(action.Message)
|
||||
return nil, c.errDenied(action.Message)
|
||||
case action.HoldAndDelegate != "":
|
||||
if action.Message != "" {
|
||||
if err := c.spac.SendAuthBanner(action.Message); err != nil {
|
||||
return nil, fmt.Errorf("error sending hold and delegate message: %w", err)
|
||||
return nil, c.errUnexpected(fmt.Errorf("error sending hold and delegate message: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,11 +381,11 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
action, err = c.fetchSSHAction(ctx, url)
|
||||
if err != nil {
|
||||
metricTerminalFetchError.Add(1)
|
||||
return nil, c.bannerError("failed to fetch next SSH action", fmt.Errorf("fetch failed from %s: %w", url, err))
|
||||
return nil, c.errBanner("failed to fetch next SSH action", fmt.Errorf("fetch failed from %s: %w", url, err))
|
||||
}
|
||||
default:
|
||||
metricTerminalMalformed.Add(1)
|
||||
return nil, c.bannerError("reached Action that had neither Accept, Reject, nor HoldAndDelegate", nil)
|
||||
return nil, c.errBanner("reached Action that had neither Accept, Reject, nor HoldAndDelegate", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,6 +422,20 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
|
||||
return perms, nil
|
||||
},
|
||||
PasswordCallback: func(cm gossh.ConnMetadata, pword []byte) (*gossh.Permissions, error) {
|
||||
// Some clients don't request 'none' authentication. Instead, they
|
||||
// immediately supply a password. We humor them by accepting the
|
||||
// password, but authenticate as usual, ignoring the actual value of
|
||||
// the password.
|
||||
return c.clientAuth(cm)
|
||||
},
|
||||
PublicKeyCallback: func(cm gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
// Some clients don't request 'none' authentication. Instead, they
|
||||
// immediately supply a public key. We humor them by accepting the
|
||||
// key, but authenticate as usual, ignoring the actual content of
|
||||
// the key.
|
||||
return c.clientAuth(cm)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +446,7 @@ func (srv *server) newConn() (*conn, error) {
|
||||
// Stop accepting new connections.
|
||||
// Connections in the auth phase are handled in handleConnPostSSHAuth.
|
||||
// Existing sessions are terminated by Shutdown.
|
||||
return nil, errDenied("tailscale: server is shutting down")
|
||||
return nil, errors.New("server is shutting down")
|
||||
}
|
||||
srv.mu.Unlock()
|
||||
c := &conn{srv: srv}
|
||||
|
Reference in New Issue
Block a user