ssh/tailssh: fall back to su if login is not an option

This works on more recent versions of Linux and has the
benefit of allowing PAM login actions to run.

Updates #11854

Signed-off-by: Percy Wegmann <percy@tailscale.com>
ox/11854
Percy Wegmann 6 months ago
parent 14ac41febc
commit fee3aeb7f2
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -12,6 +12,7 @@
package tailssh package tailssh
import ( import (
"bytes"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@ -121,22 +122,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
if isShell { if isShell {
incubatorArgs = append(incubatorArgs, "--shell") incubatorArgs = append(incubatorArgs, "--shell")
} }
// Only the macOS version of the login command supports executing a
// command, all other versions only support launching a shell
// without taking any arguments.
shouldUseLoginCmd := isShell || runtime.GOOS == "darwin"
if hostinfo.IsSELinuxEnforcing() {
// If we're running on a SELinux-enabled system, the login
// command will be unable to set the correct context for the
// shell. Fall back to using the incubator to launch the shell.
// See http://github.com/tailscale/tailscale/issues/4908.
shouldUseLoginCmd = false
}
if shouldUseLoginCmd {
if lp, err := exec.LookPath("login"); err == nil {
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
}
}
incubatorArgs = append(incubatorArgs, "--cmd="+name) incubatorArgs = append(incubatorArgs, "--cmd="+name)
if len(args) > 0 { if len(args) > 0 {
incubatorArgs = append(incubatorArgs, "--") incubatorArgs = append(incubatorArgs, "--")
@ -164,19 +150,18 @@ func (stdRWC) Close() error {
} }
type incubatorArgs struct { type incubatorArgs struct {
uid int uid int
gid int gid int
groups string groups string
localUser string localUser string
remoteUser string remoteUser string
remoteIP string remoteIP string
ttyName string ttyName string
hasTTY bool hasTTY bool
cmdName string cmdName string
isSFTP bool isSFTP bool
isShell bool isShell bool
loginCmdPath string cmdArgs []string
cmdArgs []string
} }
func parseIncubatorArgs(args []string) (a incubatorArgs) { func parseIncubatorArgs(args []string) (a incubatorArgs) {
@ -192,7 +177,6 @@ func parseIncubatorArgs(args []string) (a incubatorArgs) {
flags.StringVar(&a.cmdName, "cmd", "", "the cmd to launch (ignored in sftp mode)") flags.StringVar(&a.cmdName, "cmd", "", "the cmd to launch (ignored in sftp mode)")
flags.BoolVar(&a.isShell, "shell", false, "is launching a shell (with no cmds)") flags.BoolVar(&a.isShell, "shell", false, "is launching a shell (with no cmds)")
flags.BoolVar(&a.isSFTP, "sftp", false, "run sftp server (cmd is ignored)") flags.BoolVar(&a.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
flags.StringVar(&a.loginCmdPath, "login-cmd", "", "the path to `login` cmd")
flags.Parse(args) flags.Parse(args)
a.cmdArgs = flags.Args() a.cmdArgs = flags.Args()
return a return a
@ -231,14 +215,8 @@ func beIncubator(args []string) error {
} }
} }
euid := os.Geteuid() if handled, err := tryLoginShell(logf, ia); handled {
runningAsRoot := euid == 0 return err
if runningAsRoot && ia.loginCmdPath != "" {
// Check if we can exec into the login command instead of trying to
// incubate ourselves.
if la := ia.loginArgs(); la != nil {
return unix.Exec(ia.loginCmdPath, la, os.Environ())
}
} }
// Inform the system that we are about to log someone in. // Inform the system that we are about to log someone in.
@ -312,6 +290,143 @@ func beIncubator(args []string) error {
return err return err
} }
// tryLoginShell attempts to handle the ssh session by creating a full login
// shell. If it was able to do so, it returns true plus any error from running
// that shell. If it was unable to do so, it returns false, nil.
//
// We prefer to create a login shell using either the login command
// (e.g. /usr/bin/login) or using the su command (e.g. /usr/bin/su). A login
// shell has the advantage of running PAM authentication, which will set up the
// connected user's environment. See https://github.com/tailscale/tailscale/issues/11854.
//
// login is preferred over su because it supports the `-h` option, allowing the
// system to record the remote IP associated with the login.
//
// However, login is subject to some limitations.
//
// 1. login cannot be used to execute commands except on macOS.
// 2. On Linux and BSD, login requires a TTY to keep running.
//
// Unlike login, su often does not require a TTY, so on Linux hosts that have
// an su command which accepts the right flags, we fall back to using that when
// no TTY is available.
//
// Note - one nuance of this is that when we use login with the -h option, the
// shell will use the "remote" PAM profile. When we fall back to using "su",
// the shell will use the "login" PAM profile.
func tryLoginShell(logf logger.Logf, ia incubatorArgs) (bool, error) {
euid := os.Geteuid()
runningAsRoot := euid == 0
// Decide whether we should attempt to get a full login shell using either
// the login or su commands.
attemptLoginShell := true
switch {
case ia.isSFTP:
// If we're going to run an sFTP server, we don't want a shell
attemptLoginShell = false
case !runningAsRoot:
// We have to be root in order to create a login shell.
attemptLoginShell = false
case hostinfo.IsSELinuxEnforcing():
// If we're running on a SELinux-enabled system, neiher login nor su
// will be able to set the correct context for the shell. So, we don't
// bother trying to run them and instead fall back to using the
// incubator to launch the shell.
// See http://github.com/tailscale/tailscale/issues/4908.
attemptLoginShell = false
}
if !attemptLoginShell {
logf("not attempting login shell")
return false, nil
}
shouldUseLoginCmd := ia.isShell || runtime.GOOS == "darwin"
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
if !ia.hasTTY {
// We can only use login command if a shell was requested with a TTY. If
// there is no TTY, login exits immediately, which breaks things likes
// mosh and VSCode.
shouldUseLoginCmd = false
}
}
if shouldUseLoginCmd {
if loginCmdPath, err := exec.LookPath("login"); err == nil {
logf("using %s command", loginCmdPath)
return true, unix.Exec(loginCmdPath, ia.loginArgs(loginCmdPath), os.Environ())
}
}
// We weren't able to use login, maybe we can use su.
// Currently, we only support falling back to su on Linux. This
// potentially could work on BSDs as well, but requires testing.
canUseSU := runtime.GOOS == "linux"
if !canUseSU {
logf("not attempting su")
return false, nil
}
return tryLoginWithSU(logf, ia)
}
// tryLoginWithSU attempts to start a login shell using su instead of login. If
// su is available and supports the necessary arguments, this returns true,
// plus the result of executing su. Otherwise, it returns false, nil.
func tryLoginWithSU(logf logger.Logf, ia incubatorArgs) (bool, error) {
su, err := exec.LookPath("su")
if err != nil {
// Can't find su, don't bother trying.
logf("can't find su command")
return false, nil
}
// Get help text to inspect supported flags.
out, err := exec.Command(su, "-h").CombinedOutput()
if err != nil {
logf("%s doesn't support -h, don't use", su)
// Can't even call su -h, don't bother trying.
return false, nil
}
supportsFlag := func(flag string) bool {
return bytes.Contains(out, []byte(flag))
}
// Make sure su supports the necessary flags.
if !supportsFlag("-l") {
logf("%s doesn't support -l, don't use", su)
return false, nil
}
if !supportsFlag("-c") {
logf("%s doesn't support -c, don't use", su)
return false, nil
}
loginArgs := []string{
"-l",
}
if ia.hasTTY && supportsFlag("-P") {
// Allocate a pseudo terminal for improved security. In particular,
// this can help avoid TIOCSTI ioctl terminal injection.
loginArgs = append(loginArgs, "-P")
}
loginArgs = append(loginArgs, ia.localUser)
if !ia.isShell && ia.cmdName != "" {
// We only execute the requested command if we're not requesting a
// shell. When requesting a shell, the command is the requested shell,
// which is redundant because `su -l` will give the user their default
// shell.
loginArgs = append(loginArgs, "-c", ia.cmdName)
loginArgs = append(loginArgs, ia.cmdArgs...)
}
logf("logging in with su %+v", loginArgs)
return true, unix.Exec("/usr/bin/su", loginArgs, os.Environ())
}
const ( const (
// This controls whether we assert that our privileges were dropped // This controls whether we assert that our privileges were dropped
// using geteuid/getegid; it's a const and not an envknob because the // using geteuid/getegid; it's a const and not an envknob because the
@ -731,18 +846,11 @@ func fileExists(path string) bool {
} }
// loginArgs returns the arguments to use to exec the login binary. // loginArgs returns the arguments to use to exec the login binary.
// It returns nil if the login binary should not be used. func (ia *incubatorArgs) loginArgs(loginCmdPath string) []string {
// The login binary is only used:
// - on darwin, if the client is requesting a shell or a command.
// - on linux and BSD, if the client is requesting a shell with a TTY.
func (ia *incubatorArgs) loginArgs() []string {
if ia.isSFTP {
return nil
}
switch runtime.GOOS { switch runtime.GOOS {
case "darwin": case "darwin":
args := []string{ args := []string{
ia.loginCmdPath, loginCmdPath,
"-f", // already authenticated "-f", // already authenticated
// login typically discards the previous environment, but we want to // login typically discards the previous environment, but we want to
@ -761,29 +869,17 @@ func (ia *incubatorArgs) loginArgs() []string {
} }
return args return args
case "linux": case "linux":
if !ia.isShell || !ia.hasTTY {
// We can only use login command if a shell was requested with a TTY. If
// there is no TTY, login exits immediately, which breaks things likes
// mosh and VSCode.
return nil
}
if distro.Get() == distro.Arch && !fileExists("/etc/pam.d/remote") { if distro.Get() == distro.Arch && !fileExists("/etc/pam.d/remote") {
// See https://github.com/tailscale/tailscale/issues/4924 // See https://github.com/tailscale/tailscale/issues/4924
// //
// Arch uses a different login binary that makes the -h flag set the PAM // Arch uses a different login binary that makes the -h flag set the PAM
// service to "remote". So if they don't have that configured, don't // service to "remote". So if they don't have that configured, don't
// pass -h. // pass -h.
return []string{ia.loginCmdPath, "-f", ia.localUser, "-p"} return []string{loginCmdPath, "-f", ia.localUser, "-p"}
} }
return []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"} return []string{loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}
case "freebsd", "openbsd": case "freebsd", "openbsd":
if !ia.isShell || !ia.hasTTY { return []string{loginCmdPath, "-fp", "-h", ia.remoteIP, ia.localUser}
// We can only use login command if a shell was requested with a TTY. If
// there is no TTY, login exits immediately, which breaks things likes
// mosh and VSCode.
return nil
}
return []string{ia.loginCmdPath, "-fp", "-h", ia.remoteIP, ia.localUser}
} }
panic("unimplemented") panic("unimplemented")
} }

Loading…
Cancel
Save