Skip to content

Commit

Permalink
checking for a valid home directory now ensures that the local user has
Browse files Browse the repository at this point in the history
access and child processes fallback to the root directory ("/") in the
case that they do not
  • Loading branch information
rosstimothy authored and eriktate committed Oct 24, 2024
1 parent ae2b549 commit aae1b74
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 36 deletions.
4 changes: 4 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ const (
// HomeDirNotFound is returned when a the "teleport checkhomedir" command cannot
// find the user's home directory.
HomeDirNotFound = 254
// HomeDirNotAccessible is returned when a the "teleport checkhomedir" command has
// found the user's home directory, but the user does NOT have permissions to
// access it.
HomeDirNotAccessible = 253
)

// MaxEnvironmentFileLines is the maximum number of lines in a environment file.
Expand Down
40 changes: 33 additions & 7 deletions lib/srv/exec_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,56 @@ import (
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"syscall"
"testing"
"time"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/host"
)

func TestOSCommandPrep(t *testing.T) {
utils.RequireRoot(t)

srv := newMockServer(t)
scx := newExecServerContext(t, srv)

usr, err := user.Current()
// because CheckHomeDir now inspects access to the home directory as the actual user after a rexec,
// we need to setup a real, non-root user with a valid home directory in order for this test to
// exercise the correct paths
tempHome := t.TempDir()
require.NoError(t, os.Chmod(filepath.Dir(tempHome), 0777))

username := "test-os-command-prep"
scx.Identity.Login = username
_, err := host.UserAdd(username, nil, host.UserOpts{
Home: tempHome,
})
require.NoError(t, err)
t.Cleanup(func() {
// change homedir back so user deletion doesn't fail
changeHomeDir(t, username, tempHome)
_, err := host.UserDel(username)
require.NoError(t, err)
})

usr, err := user.Lookup(username)
require.NoError(t, err)

uid, err := strconv.Atoi(usr.Uid)
require.NoError(t, err)

require.NoError(t, os.Chown(tempHome, uid, -1))
expectedEnv := []string{
"LANG=en_US.UTF-8",
getDefaultEnvPath(strconv.Itoa(os.Geteuid()), defaultLoginDefsPath),
getDefaultEnvPath(usr.Uid, defaultLoginDefsPath),
fmt.Sprintf("HOME=%s", usr.HomeDir),
fmt.Sprintf("USER=%s", usr.Username),
fmt.Sprintf("USER=%s", username),
"SHELL=/bin/sh",
"SSH_CLIENT=10.0.0.5 4817 3022",
"SSH_CONNECTION=10.0.0.5 4817 127.0.0.1 3022",
Expand Down Expand Up @@ -99,12 +128,9 @@ func TestOSCommandPrep(t *testing.T) {
require.Equal(t, []string{"/bin/sh", "-c", "top"}, cmd.Args)
require.Equal(t, syscall.SIGKILL, cmd.SysProcAttr.Pdeathsig)

if os.Geteuid() != 0 {
t.Skip("skipping portion of test which must run as root")
}

// Missing home directory - HOME should still be set to the given
// home dir, but the command should set it's CWD to root instead.
changeHomeDir(t, username, "/wrong/place")
usr.HomeDir = "/wrong/place"
root := string(os.PathSeparator)
expectedEnv[2] = "HOME=/wrong/place"
Expand Down
128 changes: 100 additions & 28 deletions lib/srv/reexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ func (o *osWrapper) startNewParker(ctx context.Context, credential *syscall.Cred
return nil
}

const rootDirectory = "/"

func RunNetworking() (errw io.Writer, code int, err error) {
// SIGQUIT is used by teleport to initiate graceful shutdown, waiting for
// existing exec sessions to close before ending the process. For this to
Expand Down Expand Up @@ -619,10 +621,13 @@ func RunNetworking() (errw io.Writer, code int, err error) {
}

// Create a minimal default environment for the user.
homeDir := localUser.HomeDir
if !utils.IsDir(homeDir) {
homeDir = "/"
workingDir := rootDirectory

hasAccess, err := CheckHomeDir(localUser)
if hasAccess && err == nil {
workingDir = localUser.HomeDir
}

os.Setenv("HOME", localUser.HomeDir)
os.Setenv("USER", c.Login)

Expand All @@ -638,8 +643,8 @@ func RunNetworking() (errw io.Writer, code int, err error) {
}

// Ensure that the working directory is one that the local user has access to.
if err := os.Chdir(homeDir); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set working directory for networking process")
if err := os.Chdir(workingDir); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set working directory for networking process: %s", workingDir)
}

// Build request listener from first extra file that was passed to command.
Expand Down Expand Up @@ -872,16 +877,21 @@ func getConnFile(conn net.Conn) (*os.File, error) {
}
}

// runCheckHomeDir check's if the active user's $HOME dir exists.
// runCheckHomeDir checks if the active user's $HOME dir exists and is accessible.
func runCheckHomeDir() (errw io.Writer, code int, err error) {
home, err := os.UserHomeDir()
if err != nil {
return io.Discard, teleport.HomeDirNotFound, nil
}
if !utils.IsDir(home) {
return io.Discard, teleport.HomeDirNotFound, nil
code = teleport.RemoteCommandSuccess
if err := hasAccessibleHomeDir(); err != nil {
switch {
case trace.IsNotFound(err), trace.IsBadParameter(err):
code = teleport.HomeDirNotFound
case trace.IsAccessDenied(err):
code = teleport.HomeDirNotAccessible
default:
code = teleport.RemoteCommandFailure
}
}
return io.Discard, teleport.RemoteCommandSuccess, nil

return io.Discard, code, nil
}

// runPark does nothing, forever.
Expand Down Expand Up @@ -1055,18 +1065,20 @@ func buildCommand(c *ExecCommand, localUser *user.User, tty *os.File, pamEnviron
// Set the command's cwd to the user's $HOME, or "/" if
// they don't have an existing home dir.
// TODO (atburke): Generalize this to support Windows.
exists, err := CheckHomeDir(localUser)
hasAccess, err := CheckHomeDir(localUser)
if err != nil {
return nil, trace.Wrap(err)
} else if exists {
}

if hasAccess {
cmd.Dir = localUser.HomeDir
} else if !exists {
} else {
// Write failure to find home dir to stdout, same as OpenSSH.
msg := fmt.Sprintf("Could not set shell's cwd to home directory %q, defaulting to %q\n", localUser.HomeDir, string(os.PathSeparator))
msg := fmt.Sprintf("Could not set shell's cwd to home directory %q, defaulting to %q\n", localUser.HomeDir, rootDirectory)
if _, err := cmd.Stdout.Write([]byte(msg)); err != nil {
return nil, trace.Wrap(err)
}
cmd.Dir = string(os.PathSeparator)
cmd.Dir = rootDirectory
}

// Only set process credentials if the UID/GID of the requesting user are
Expand Down Expand Up @@ -1200,16 +1212,73 @@ func copyCommand(ctx *ServerContext, cmdmsg *ExecCommand) {
}
}

// CheckHomeDir checks if the user's home dir exists
func coerceHomeDirError(usr *user.User, err error) error {
if os.IsNotExist(err) {
return trace.NotFound("home directory %q not found for user %q", usr.HomeDir, usr.Name)
}

if os.IsPermission(err) {
return trace.AccessDenied("%q does not have permission to access %q", usr.Name, usr.HomeDir)
}

return err
}

// hasAccessibleHomeDir checks if the current user has access to an existing home directory.
func hasAccessibleHomeDir() error {
// this should usually be fetching a cached value
currentUser, err := user.Current()
if err != nil {
return trace.Wrap(err)
}

fi, err := os.Stat(currentUser.HomeDir)
if err != nil {
return trace.Wrap(coerceHomeDirError(currentUser, err))
}

if !fi.IsDir() {
return trace.BadParameter("%q is not a directory", currentUser.HomeDir)
}

cwd, err := os.Getwd()
if err != nil {
return trace.Wrap(err)
}
// make sure we return to the original working directory
defer os.Chdir(cwd)

// attemping to cd into the target directory is the easiest, cross-platform way to test
// whether or not the current user has access
if err := os.Chdir(currentUser.HomeDir); err != nil {
return trace.Wrap(coerceHomeDirError(currentUser, err))
}

return nil
}

// CheckHomeDir checks if the user's home directory exists and is accessible to the user. Only catastrophic
// errors will be returned, which means a missing, inaccessible, or otherwise invalid home directory will result
// in a return of (false, nil)
func CheckHomeDir(localUser *user.User) (bool, error) {
if fi, err := os.Stat(localUser.HomeDir); err == nil {
return fi.IsDir(), nil
currentUser, err := user.Current()
if err != nil {
return false, trace.Wrap(err)
}

// don't spawn a subcommand if already running as the user in question
if currentUser.Uid == localUser.Uid {
if err := hasAccessibleHomeDir(); err != nil {
if trace.IsNotFound(err) || trace.IsAccessDenied(err) || trace.IsBadParameter(err) {
return false, nil
}

return false, trace.Wrap(err)
}

return true, nil
}

// In some environments, the user's home directory exists but isn't visible to
// root, e.g. /home is mounted to an nfs export with root_squash enabled.
// In case we are in that scenario, re-exec teleport as the user to check
// if the home dir actually does exist.
executable, err := os.Executable()
if err != nil {
return false, trace.Wrap(err)
Expand All @@ -1225,6 +1294,7 @@ func CheckHomeDir(localUser *user.User) (bool, error) {
Path: executable,
Args: []string{executable, teleport.CheckHomeDirSubCommand},
Env: []string{"HOME=" + localUser.HomeDir},
Dir: rootDirectory,
SysProcAttr: &syscall.SysProcAttr{
Setsid: true,
Credential: credential,
Expand All @@ -1235,11 +1305,13 @@ func CheckHomeDir(localUser *user.User) (bool, error) {
reexecCommandOSTweaks(cmd)

if err := cmd.Run(); err != nil {
if cmd.ProcessState.ExitCode() == teleport.HomeDirNotFound {
return false, nil
if cmd.ProcessState.ExitCode() == teleport.RemoteCommandFailure {
return false, trace.Wrap(err)
}
return false, trace.Wrap(err)

return false, nil
}

return true, nil
}

Expand Down
70 changes: 70 additions & 0 deletions lib/srv/reexec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"syscall"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/agent"

Expand Down Expand Up @@ -410,3 +411,72 @@ func testX11Forward(ctx context.Context, t *testing.T, proc *networking.Process,
require.NoError(t, err)
require.Equal(t, fakeXauthEntry, readXauthEntry)
}

func TestRootCheckHomeDir(t *testing.T) {
utils.RequireRoot(t)

tmp := t.TempDir()
require.NoError(t, os.Chmod(filepath.Dir(tmp), 0777))
require.NoError(t, os.Chmod(tmp, 0777))

home := filepath.Join(tmp, "home")
noAccess := filepath.Join(tmp, "no_access")
file := filepath.Join(tmp, "file")
notFound := filepath.Join(tmp, "not_found")

require.NoError(t, os.Mkdir(home, 0700))
require.NoError(t, os.Mkdir(noAccess, 0700))
_, err := os.Create(file)
require.NoError(t, err)

login := utils.GenerateLocalUsername(t)
_, err = host.UserAdd(login, nil, host.UserOpts{Home: home})
require.NoError(t, err)
t.Cleanup(func() {
// change back to accessible home so deletion works
changeHomeDir(t, login, home)
_, err := host.UserDel(login)
require.NoError(t, err)
})

testUser, err := user.Lookup(login)
require.NoError(t, err)

uid, err := strconv.Atoi(testUser.Uid)
require.NoError(t, err)

gid, err := strconv.Atoi(testUser.Gid)
require.NoError(t, err)

require.NoError(t, os.Chown(home, uid, gid))
require.NoError(t, os.Chown(file, uid, gid))

hasAccess, err := CheckHomeDir(testUser)
require.NoError(t, err)
require.True(t, hasAccess)

changeHomeDir(t, login, file)
hasAccess, err = CheckHomeDir(testUser)
require.NoError(t, err)
require.False(t, hasAccess)

changeHomeDir(t, login, notFound)
hasAccess, err = CheckHomeDir(testUser)
require.NoError(t, err)
require.False(t, hasAccess)

changeHomeDir(t, login, noAccess)
hasAccess, err = CheckHomeDir(testUser)
require.NoError(t, err)
require.False(t, hasAccess)
}

func changeHomeDir(t *testing.T, username, home string) {
usermodBin, err := exec.LookPath("usermod")
assert.NoError(t, err, "usermod binary must be present")

cmd := exec.Command(usermodBin, "--home", home, username)
_, err = cmd.CombinedOutput()
assert.NoError(t, err, "changing home should not error")
assert.Equal(t, 0, cmd.ProcessState.ExitCode(), "changing home should exit 0")
}
2 changes: 1 addition & 1 deletion lib/srv/regular/sshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,7 @@ func (s *Server) HandleNewConn(ctx context.Context, ccx *sshutils.ConnectionCont
// Create host user.
created, userCloser, err := s.termHandlers.SessionRegistry.UpsertHostUser(identityContext)
if err != nil {
log.Infof("error while creating host users: %s", err)
log.Warnf("error while creating host users: %s", err)
}

// Indicate that the user was created by Teleport.
Expand Down

0 comments on commit aae1b74

Please sign in to comment.