@ -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 , "--" )
@ -175,7 +161,6 @@ type incubatorArgs struct {
cmdName string
cmdName string
isSFTP bool
isSFTP bool
isShell bool
isShell bool
loginCmdPath string
cmdArgs [ ] string
cmdArgs [ ] string
}
}
@ -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" )
}
}