Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fallback to root when user's home directory is not accessible #47524

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there was an assumption baked in that CheckHomeDir would always be called by the root/primary user, but I didn't observe that during testing. In fact I never actually saw the subcommand getting spawned while testing ssh connections through the web UI 🤔 This check prevents dropping into another sub process if we're already running as the target user

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
Loading