Files
teleport/lib/srv/reexec.go
T
Zac Bergquist 2ae9d770fc Refactor tctl's dependencies (#22693)
* Move configuration from lib/service to lib/service/servicecfg

The new servicecfg package will hold only configuration for services.
This will allow other packages (like tctl and tsh) to depend on
servicecfg without pulling in all of lib/service (which has a number
of platform-specific details).

This is the first step towards being able to build tctl for Windows.

* Move PAM and BPF config into servicecfg

This breaks a compile-time dependency on BPF/PAM for tctl.
2023-03-09 17:48:36 +00:00

1054 lines
33 KiB
Go

/*
Copyright 2020-2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package srv
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auditd"
"github.com/gravitational/teleport/lib/pam"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/shell"
"github.com/gravitational/teleport/lib/srv/uacc"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/utils"
)
// FileFD is a file descriptor passed down from a parent process when
// Teleport is re-executing itself.
type FileFD = uintptr
const (
// CommandFile is used to pass the command and arguments that the
// child process should execute from the parent process.
CommandFile FileFD = 3 + iota
// ContinueFile is used to communicate to the child process that
// it can continue after the parent process assigns a cgroup to the
// child process.
ContinueFile
// TerminateFile is used to communicate to the child process that
// the interactive terminal should be killed as the client ended the
// SSH session and without termination the terminal process will be assigned
// to pid 1 and "live forever". Killing the shell should not prevent processes
// preventing SIGHUP to be reassigned (ex. processes running with nohup).
TerminateFile
// X11File is used to communicate to the parent process that the child
// process has set up X11 forwarding.
X11File
// PTYFile is a PTY the parent process passes to the child process.
PTYFile
// TTYFile is a TTY the parent process passes to the child process.
TTYFile
// FirstExtraFile is the first file descriptor that will be valid when
// extra files are passed to child processes without a terminal.
FirstExtraFile = X11File + 1
)
// ExecCommand contains the payload to "teleport exec" which will be used to
// construct and execute a shell.
type ExecCommand struct {
// Command is the command to execute. If an interactive session is being
// requested, will be empty.
Command string `json:"command"`
// DestinationAddress is the target address to dial to.
DestinationAddress string `json:"dst_addr"`
// Username is the username associated with the Teleport identity.
Username string `json:"username"`
// Login is the local *nix account.
Login string `json:"login"`
// Roles is the list of Teleport roles assigned to the Teleport identity.
Roles []string `json:"roles"`
// ClusterName is the name of the Teleport cluster.
ClusterName string `json:"cluster_name"`
// Terminal indicates if a TTY has been allocated for the session. This is
// typically set if either a shell was requested or a TTY was explicitly
// allocated for an exec request.
Terminal bool `json:"term"`
// TerminalName is the name of TTY terminal, ex: /dev/tty1.
// Currently, this field is used by auditd.
TerminalName string `json:"terminal_name"`
// ClientAddress contains IP address of the connected client.
// Currently, this field is used by auditd.
ClientAddress string `json:"client_address"`
// RequestType is the type of request: either "exec" or "shell". This will
// be used to control where to connect std{out,err} based on the request
// type: "exec" or "shell".
RequestType string `json:"request_type"`
// PAMConfig is the configuration data that needs to be passed to the child and then to PAM modules.
PAMConfig *PAMConfig `json:"pam_config,omitempty"`
// Environment is a list of environment variables to add to the defaults.
Environment []string `json:"environment"`
// PermitUserEnvironment is set to allow reading in ~/.tsh/environment
// upon login.
PermitUserEnvironment bool `json:"permit_user_environment"`
// IsTestStub is used by tests to mock the shell.
IsTestStub bool `json:"is_test_stub"`
// UserCreatedByTeleport is true when the system user was created by Teleport user auto-provision.
UserCreatedByTeleport bool
// UaccMetadata contains metadata needed for user accounting.
UaccMetadata UaccMetadata `json:"uacc_meta"`
// X11Config contains an xauth entry to be added to the command user's xauthority.
X11Config X11Config `json:"x11_config"`
// ExtraFilesLen is the number of extra files that are inherited from
// the parent process. These files start at file descriptor 3 of the
// child process, and are only valid for processes without a terminal.
ExtraFilesLen int `json:"extra_files_len"`
}
// PAMConfig represents all the configuration data that needs to be passed to the child.
type PAMConfig struct {
// UsePAMAuth specifies whether to trigger the "auth" PAM modules from the
// policy.
UsePAMAuth bool `json:"use_pam_auth"`
// ServiceName is the name of the PAM service requested if PAM is enabled.
ServiceName string `json:"service_name"`
// Environment represents env variables to pass to PAM.
Environment map[string]string `json:"environment"`
}
// X11Config contains information used by the child process to set up X11 forwarding.
type X11Config struct {
// XAuthEntry contains xauth data used for X11 forwarding.
XAuthEntry x11.XAuthEntry `json:"xauth_entry,omitempty"`
// XServerUnixSocket is the name of an open XServer unix socket used for X11 forwarding.
XServerUnixSocket string `json:"xserver_unix_socket"`
}
// UaccMetadata contains information the child needs from the parent for user accounting.
type UaccMetadata struct {
// The hostname of the node.
Hostname string `json:"hostname"`
// RemoteAddr is the address of the remote host.
RemoteAddr [4]int32 `json:"remote_addr"`
// UtmpPath is the path of the system utmp database.
UtmpPath string `json:"utmp_path,omitempty"`
// WtmpPath is the path of the system wtmp log.
WtmpPath string `json:"wtmp_path,omitempty"`
}
// RunCommand reads in the command to run from the parent process (over a
// pipe) then constructs and runs the command.
func RunCommand() (errw io.Writer, code int, err error) {
// errorWriter is used to return any error message back to the client. By
// default, it writes to stdout, but if a TTY is allocated, it will write
// to it instead.
errorWriter := os.Stdout
// Parent sends the command payload in the third file descriptor.
cmdfd := os.NewFile(CommandFile, fmt.Sprintf("/proc/self/fd/%d", CommandFile))
if cmdfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found")
}
contfd := os.NewFile(ContinueFile, fmt.Sprintf("/proc/self/fd/%d", ContinueFile))
if contfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("continue pipe not found")
}
termiantefd := os.NewFile(TerminateFile, fmt.Sprintf("/proc/self/fd/%d", TerminateFile))
if termiantefd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("terminate pipe not found")
}
// Read in the command payload.
var b bytes.Buffer
_, err = b.ReadFrom(cmdfd)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
var c ExecCommand
err = json.Unmarshal(b.Bytes(), &c)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
auditdMsg := auditd.Message{
SystemUser: c.Login,
TeleportUser: c.Username,
ConnAddress: c.ClientAddress,
TTYName: c.TerminalName,
}
if err := auditd.SendEvent(auditd.AuditUserLogin, auditd.Success, auditdMsg); err != nil {
// Currently, this logs nothing. Related issue https://github.com/gravitational/teleport/issues/17318
log.WithError(err).Debugf("failed to send user start event to auditd: %v", err)
}
defer func() {
if err != nil {
if errors.Is(err, user.UnknownUserError(c.Login)) {
if err := auditd.SendEvent(auditd.AuditUserErr, auditd.Failed, auditdMsg); err != nil {
log.WithError(err).Debugf("failed to send UserErr event to auditd: %v", err)
}
return
}
}
if err := auditd.SendEvent(auditd.AuditUserEnd, auditd.Success, auditdMsg); err != nil {
log.WithError(err).Debugf("failed to send UserEnd event to auditd: %v", err)
}
}()
var tty *os.File
var pty *os.File
uaccEnabled := false
// If a terminal was requested, file descriptors 6 and 7 always point to the
// PTY and TTY. Extract them and set the controlling TTY. Otherwise, connect
// std{in,out,err} directly.
if c.Terminal {
pty = os.NewFile(PTYFile, fmt.Sprintf("/proc/self/fd/%d", PTYFile))
tty = os.NewFile(TTYFile, fmt.Sprintf("/proc/self/fd/%d", TTYFile))
if pty == nil || tty == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("pty and tty not found")
}
errorWriter = tty
err = uacc.Open(c.UaccMetadata.UtmpPath, c.UaccMetadata.WtmpPath, c.Login, c.UaccMetadata.Hostname, c.UaccMetadata.RemoteAddr, tty)
// uacc support is best-effort, only enable it if Open is successful.
// Currently, there is no way to log this error out-of-band with the
// command output, so for now we essentially ignore it.
if err == nil {
uaccEnabled = true
}
}
// If PAM is enabled, open a PAM context. This has to be done before anything
// else because PAM is sometimes used to create the local user used to
// launch the shell under.
var pamEnvironment []string
if c.PAMConfig != nil {
// Connect std{in,out,err} to the TTY if it's a shell request, otherwise
// discard std{out,err}. If this was not done, things like MOTD would be
// printed for "exec" requests.
var stdin io.Reader
var stdout io.Writer
var stderr io.Writer
if c.RequestType == sshutils.ShellRequest {
stdin = tty
stdout = tty
stderr = tty
} else {
stdin = os.Stdin
stdout = io.Discard
stderr = io.Discard
}
// Open the PAM context.
pamContext, err := pam.Open(&servicecfg.PAMConfig{
ServiceName: c.PAMConfig.ServiceName,
UsePAMAuth: c.PAMConfig.UsePAMAuth,
Login: c.Login,
// Set Teleport specific environment variables that PAM modules
// like pam_script.so can pick up to potentially customize the
// account/session.
Env: c.PAMConfig.Environment,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
})
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
defer pamContext.Close()
// Save off any environment variables that come from PAM.
pamEnvironment = pamContext.Environment()
}
localUser, err := user.Lookup(c.Login)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Build the actual command that will launch the shell.
cmd, err := buildCommand(&c, localUser, tty, pty, pamEnvironment)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Wait until the continue signal is received from Teleport signaling that
// the child process has been placed in a cgroup.
err = waitForContinue(contfd)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// If we're planning on changing credentials, we should first park an
// innocuous process with the same UID and then check the user database
// again, to avoid it getting deleted under our nose.
parkerCtx, parkerCancel := context.WithCancel(context.Background())
defer parkerCancel()
osPack := newOsWrapper()
if c.UserCreatedByTeleport {
// Parker is only needed when the user was created by Teleport.
err := osPack.startNewParker(
parkerCtx,
cmd.SysProcAttr.Credential,
c.Login, &systemUser{u: localUser})
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
}
if c.X11Config.XServerUnixSocket != "" {
// Set the open XServer unix socket's owner to the localuser
// to prevent a potential privilege escalation vulnerability.
uid, err := strconv.Atoi(localUser.Uid)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
gid, err := strconv.Atoi(localUser.Gid)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
if err := os.Chown(c.X11Config.XServerUnixSocket, uid, gid); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Update localUser's xauth database for X11 forwarding. We set
// cmd.SysProcAttr.Setsid, cmd.Env, and cmd.Dir so that the xauth command
// acts as if called within the following shell/exec, so that the
// xauthority files is put into the correct place ($HOME/.Xauthority)
// with the right permissions.
removeCmd := x11.NewXAuthCommand(context.Background(), "")
removeCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: cmd.SysProcAttr.Credential,
}
removeCmd.Env = cmd.Env
removeCmd.Dir = cmd.Dir
if err := removeCmd.RemoveEntries(c.X11Config.XAuthEntry.Display); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
addCmd := x11.NewXAuthCommand(context.Background(), "")
addCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: cmd.SysProcAttr.Credential,
}
addCmd.Env = cmd.Env
addCmd.Dir = cmd.Dir
if err := addCmd.AddEntry(c.X11Config.XAuthEntry); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Set $DISPLAY so that XServer requests forwarded to the X11 unix listener.
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", x11.DisplayEnv, c.X11Config.XAuthEntry.Display.String()))
// Open x11rdy fd to signal parent process once X11 forwarding is set up.
x11rdyfd := os.NewFile(X11File, fmt.Sprintf("/proc/self/fd/%d", X11File))
if x11rdyfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("continue pipe not found")
}
// Write a single byte to signal to the parent process that X11 forwarding is set up.
if _, err := x11rdyfd.Write([]byte{0}); err != nil {
if err2 := x11rdyfd.Close(); err2 != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.NewAggregate(err, err2)
}
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
if err := x11rdyfd.Close(); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
}
if err := setNeutralOOMScore(); err != nil {
log.WithError(err).Warnf("failed to adjust OOM score")
}
// Start the command.
if err := cmd.Start(); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
parkerCancel()
err = waitForShell(termiantefd, cmd)
if uaccEnabled {
uaccErr := uacc.Close(c.UaccMetadata.UtmpPath, c.UaccMetadata.WtmpPath, tty)
if uaccErr != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(uaccErr)
}
}
return io.Discard, exitCode(err), trace.Wrap(err)
}
// waitForShell waits either for the command to return or the kill signal from the parent Teleport process.
func waitForShell(termiantefd *os.File, cmd *exec.Cmd) error {
terminateChan := make(chan error)
go func() {
buf := make([]byte, 1)
// Wait for the terminate file descriptor to be closed. The FD will be closed when Teleport
// parent process wants to terminate the remote command and all childs.
_, err := termiantefd.Read(buf)
if err == io.EOF {
// Kill the shell process
err = trace.Errorf("shell process has been killed: %w", cmd.Process.Kill())
} else {
err = trace.Errorf("failed to read from terminate file: %w", err)
}
terminateChan <- err
}()
go func() {
// Wait for the command to exit. It doesn't make sense to print an error
// message here because the shell has successfully started. If an error
// occurred during shell execution or the shell exits with an error (like
// running exit 2), the shell will print an error if appropriate and return
// an exit code.
err := cmd.Wait()
terminateChan <- err
}()
// Wait only for the first error.
// If the command returns then we don't need to wait for the error from cmd.Process.Kill().
// If the command is being killed, then we don't care about the error code.
err := <-terminateChan
return err
}
// osWrapper wraps system calls, so we can replace them in tests.
type osWrapper struct {
LookupGroup func(name string) (*user.Group, error)
LookupUser func(username string) (*user.User, error)
CommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}
func newOsWrapper() *osWrapper {
return &osWrapper{
LookupGroup: user.LookupGroup,
LookupUser: user.Lookup,
CommandContext: exec.CommandContext,
}
}
// userInfo wraps user.User data into an interface, so we can override
// returned results in tests.
type userInfo interface {
GID() string
UID() string
GroupIds() ([]string, error)
}
type systemUser struct {
u *user.User
}
func (s *systemUser) GID() string {
return s.u.Gid
}
func (s *systemUser) UID() string {
return s.u.Uid
}
func (s *systemUser) GroupIds() ([]string, error) {
return s.u.GroupIds()
}
// startNewParker starts a new parker process only if the requested user has been created
// by Teleport. Otherwise, does nothing.
func (o *osWrapper) startNewParker(ctx context.Context, credential *syscall.Credential,
loginAsUser string, localUser userInfo,
) error {
if credential == nil {
// Empty credential, no reason to start the parker.
return nil
}
group, err := o.LookupGroup(types.TeleportServiceGroup)
if err != nil {
if isUnknownGroupError(err, types.TeleportServiceGroup) {
// The service group doesn't exist. Auto-provision is disabled, do nothing.
return nil
}
return trace.Wrap(err)
}
groups, err := localUser.GroupIds()
if err != nil {
return trace.Wrap(err)
}
found := false
for _, localUserGroup := range groups {
if localUserGroup == group.Gid {
found = true
break
}
}
if !found {
// Check if the new user guid matches the TeleportServiceGroup. If not
// this user hasn't been created by Teleport, and we don't need the parker.
return nil
}
if err := o.newParker(ctx, *credential); err != nil {
return trace.Wrap(err)
}
localUserCheck, err := o.LookupUser(loginAsUser)
if err != nil {
return trace.Wrap(err)
}
if localUser.UID() != localUserCheck.Uid || localUser.GID() != localUserCheck.Gid {
return trace.BadParameter("user %q has been changed", loginAsUser)
}
return nil
}
// RunForward reads in the command to run from the parent process (over a
// pipe) then port forwards.
func RunForward() (errw io.Writer, code int, err error) {
// errorWriter is used to return any error message back to the client.
// Use stderr so that it's not forwarded to the remote client.
errorWriter := os.Stderr
// Parent sends the command payload in the third file descriptor.
cmdfd := os.NewFile(CommandFile, fmt.Sprintf("/proc/self/fd/%d", CommandFile))
if cmdfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found")
}
// Read in the command payload.
var b bytes.Buffer
_, err = b.ReadFrom(cmdfd)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
var c ExecCommand
err = json.Unmarshal(b.Bytes(), &c)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// If PAM is enabled, open a PAM context. This has to be done before anything
// else because PAM is sometimes used to create the local user used to
// launch the shell under.
if c.PAMConfig != nil {
// Open the PAM context.
pamContext, err := pam.Open(&servicecfg.PAMConfig{
ServiceName: c.PAMConfig.ServiceName,
Login: c.Login,
Stdin: os.Stdin,
Stdout: io.Discard,
Stderr: io.Discard,
// Set Teleport specific environment variables that PAM modules
// like pam_script.so can pick up to potentially customize the
// account/session.
Env: c.PAMConfig.Environment,
})
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
defer pamContext.Close()
}
// Connect to the target host.
conn, err := net.Dial("tcp", c.DestinationAddress)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
defer conn.Close()
// Start copy routines that copy from channel to stdin pipe and from stdout
// pipe to channel.
errorCh := make(chan error, 2)
go func() {
defer conn.Close()
defer os.Stdout.Close()
defer os.Stdin.Close()
_, err := io.Copy(os.Stdout, conn)
errorCh <- err
}()
go func() {
defer conn.Close()
defer os.Stdout.Close()
defer os.Stdin.Close()
_, err := io.Copy(conn, os.Stdin)
errorCh <- err
}()
// Block until copy is complete in either direction. The other direction
// will get cleaned up automatically.
if err = <-errorCh; err != nil && err != io.EOF {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
return io.Discard, teleport.RemoteCommandSuccess, nil
}
// runCheckHomeDir check's if the active user's $HOME dir exists.
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
}
return io.Discard, teleport.RemoteCommandSuccess, nil
}
// runPark does nothing, forever.
func runPark() (errw io.Writer, code int, err error) {
// Do not replace this with an empty select because there are no other
// goroutines running so it will panic.
for {
time.Sleep(time.Hour)
}
}
// RunAndExit will run the requested command and then exit. This wrapper
// allows Run{Command,Forward} to use defers and makes sure error messages
// are consistent across both.
func RunAndExit(commandType string) {
var w io.Writer
var code int
var err error
switch commandType {
case teleport.ExecSubCommand:
w, code, err = RunCommand()
case teleport.ForwardSubCommand:
w, code, err = RunForward()
case teleport.CheckHomeDirSubCommand:
w, code, err = runCheckHomeDir()
case teleport.ParkSubCommand:
w, code, err = runPark()
default:
w, code, err = os.Stderr, teleport.RemoteCommandFailure, fmt.Errorf("unknown command type: %v", commandType)
}
if err != nil {
s := fmt.Sprintf("Failed to launch: %v.\r\n", err)
io.Copy(w, bytes.NewBufferString(s))
}
os.Exit(code)
}
// IsReexec determines if the current process is a teleport reexec command.
// Used by tests to reroute the execution to RunAndExit.
func IsReexec() bool {
if len(os.Args) == 2 {
switch os.Args[1] {
case teleport.ExecSubCommand, teleport.ForwardSubCommand, teleport.CheckHomeDirSubCommand,
teleport.ParkSubCommand, teleport.SFTPSubCommand:
return true
}
}
return false
}
// buildCommand constructs a command that will execute the users shell. This
// function is run by Teleport while it's re-executing.
func buildCommand(c *ExecCommand, localUser *user.User, tty *os.File, pty *os.File, pamEnvironment []string) (*exec.Cmd, error) {
var cmd exec.Cmd
// Get the login shell for the user (or fallback to the default).
shellPath, err := shell.GetLoginShell(c.Login)
if err != nil {
log.Debugf("Failed to get login shell for %v: %v. Using default: %v.",
c.Login, err, shell.DefaultShell)
}
if c.IsTestStub {
shellPath = "/bin/sh"
}
// If no command was given, configure a shell to run in 'login' mode.
// Otherwise, execute a command through the shell.
if c.Command == "" {
// Set the path to the path of the shell.
cmd.Path = shellPath
// Configure the shell to run in 'login' mode. From OpenSSH source:
// "If we have no command, execute the shell. In this case, the shell
// name to be passed in argv[0] is preceded by '-' to indicate that
// this is a login shell."
// https://github.com/openssh/openssh-portable/blob/master/session.c
cmd.Args = []string{"-" + filepath.Base(shellPath)}
} else {
// Execute commands like OpenSSH does:
// https://github.com/openssh/openssh-portable/blob/master/session.c
cmd.Path = shellPath
cmd.Args = []string{shellPath, "-c", c.Command}
}
// Create default environment for user.
cmd.Env = []string{
"LANG=en_US.UTF-8",
getDefaultEnvPath(localUser.Uid, defaultLoginDefsPath),
"HOME=" + localUser.HomeDir,
"USER=" + c.Login,
"SHELL=" + shellPath,
}
// Add in Teleport specific environment variables.
cmd.Env = append(cmd.Env, c.Environment...)
// If the server allows reading in of ~/.tsh/environment read it in
// and pass environment variables along to new session.
if c.PermitUserEnvironment {
filename := filepath.Join(localUser.HomeDir, ".tsh", "environment")
userEnvs, err := utils.ReadEnvironmentFile(filename)
if err != nil {
return nil, trace.Wrap(err)
}
cmd.Env = append(cmd.Env, userEnvs...)
}
// If any additional environment variables come from PAM, apply them as well.
cmd.Env = append(cmd.Env, pamEnvironment...)
// If a terminal was requested, connect std{in,out,err} to the TTY and set
// the controlling TTY. Otherwise, connect std{in,out,err} to
// os.Std{in,out,err}.
if c.Terminal {
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Setctty: true,
// Note: leaving Ctty empty will default it to stdin fd, which is
// set to our tty above.
}
} else {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
// If a terminal was not requested, and extra files were specified
// to be passed to the child, open them so that they can be passed
// to the grandchild.
if c.ExtraFilesLen > 0 {
cmd.ExtraFiles = make([]*os.File, c.ExtraFilesLen)
for i := 0; i < c.ExtraFilesLen; i++ {
fd := FirstExtraFile + uintptr(i)
f := os.NewFile(fd, strconv.Itoa(int(fd)))
if f == nil {
return nil, trace.NotFound("extra file %d not found", fd)
}
cmd.ExtraFiles[i] = f
}
}
}
// 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)
if err != nil {
return nil, trace.Wrap(err)
} else if exists {
cmd.Dir = localUser.HomeDir
} else if !exists {
// 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))
if _, err := cmd.Stdout.Write([]byte(msg)); err != nil {
return nil, trace.Wrap(err)
}
cmd.Dir = string(os.PathSeparator)
}
// Only set process credentials if the UID/GID of the requesting user are
// different than the process (Teleport).
//
// Note, the above is important because setting the credentials struct
// triggers calling of the SETUID and SETGID syscalls during process start.
// If the caller does not have permission to call those two syscalls (for
// example, if Teleport is started from a shell), this will prevent the
// process from spawning shells with the error: "operation not permitted". To
// workaround this, the credentials struct is only set if the credentials
// are different from the process itself. If the credentials are not, simply
// pick up the ambient credentials of the process.
credential, err := getCmdCredential(localUser)
if err != nil {
return nil, trace.Wrap(err)
}
if os.Getuid() != int(credential.Uid) || os.Getgid() != int(credential.Gid) {
cmd.SysProcAttr.Credential = credential
log.Debugf("Creating process with UID %v, GID: %v, and Groups: %v.",
credential.Uid, credential.Gid, credential.Groups)
} else {
log.Debugf("Creating process with ambient credentials UID %v, GID: %v, Groups: %v.",
credential.Uid, credential.Gid, credential.Groups)
}
// Perform OS-specific tweaks to the command.
userCommandOSTweaks(&cmd)
return &cmd, nil
}
// ConfigureCommand creates a command fully configured to execute. This
// function is used by Teleport to re-execute itself and pass whatever data
// is need to the child to actually execute the shell.
func ConfigureCommand(ctx *ServerContext, extraFiles ...*os.File) (*exec.Cmd, error) {
// Create a os.Pipe and start copying over the payload to execute. While the
// pipe buffer is quite large (64k) some users have run into the pipe
// blocking writes on much smaller buffers (7k) leading to Teleport being
// unable to run some exec commands.
//
// To not depend on the OS implementation of a pipe, instead the copy should
// be non-blocking. The io.Copy will be closed when either when the child
// process has fully read in the payload or the process exits with an error
// (and closes all child file descriptors).
//
// See the below for details.
//
// https://man7.org/linux/man-pages/man7/pipe.7.html
cmdmsg, err := ctx.ExecCommand()
if err != nil {
return nil, trace.Wrap(err)
}
if !cmdmsg.Terminal {
cmdmsg.ExtraFilesLen = len(extraFiles)
}
cmdbytes, err := json.Marshal(cmdmsg)
if err != nil {
return nil, trace.Wrap(err)
}
go copyCommand(ctx, cmdbytes)
// Find the Teleport executable and its directory on disk.
executable, err := os.Executable()
if err != nil {
return nil, trace.Wrap(err)
}
executableDir, _ := filepath.Split(executable)
// The channel type determines the subcommand to execute (execution or
// port forwarding).
subCommand := teleport.ExecSubCommand
if ctx.ChannelType == teleport.ChanDirectTCPIP {
subCommand = teleport.ForwardSubCommand
}
// Build the list of arguments to have Teleport re-exec itself. The "-d" flag
// is appended if Teleport is running in debug mode.
args := []string{executable, subCommand}
// Build the "teleport exec" command.
cmd := &exec.Cmd{
Path: executable,
Args: args,
Dir: executableDir,
ExtraFiles: []*os.File{
ctx.cmdr,
ctx.contr,
ctx.killShellr,
ctx.x11rdyw,
},
}
// Add extra files if applicable.
if len(extraFiles) > 0 {
cmd.ExtraFiles = append(cmd.ExtraFiles, extraFiles...)
}
// Perform OS-specific tweaks to the command.
reexecCommandOSTweaks(cmd)
return cmd, nil
}
// copyCommand will copy the provided command to the child process over the
// pipe attached to the context.
func copyCommand(ctx *ServerContext, cmdbytes []byte) {
defer func() {
err := ctx.cmdw.Close()
if err != nil {
log.Errorf("Failed to close command pipe: %v.", err)
}
// Set to nil so the close in the context doesn't attempt to re-close.
ctx.cmdw = nil
}()
// Write command bytes to pipe. The child process will read the command
// to execute from this pipe.
_, err := io.Copy(ctx.cmdw, bytes.NewReader(cmdbytes))
if err != nil {
log.Errorf("Failed to copy command over pipe: %v.", err)
return
}
}
// CheckHomeDir checks if the user's home dir exists
func CheckHomeDir(localUser *user.User) (bool, error) {
if fi, err := os.Stat(localUser.HomeDir); err == nil {
return fi.IsDir(), 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)
}
credential, err := getCmdCredential(localUser)
if err != nil {
return false, trace.Wrap(err)
}
// Build the "teleport exec" command.
cmd := &exec.Cmd{
Path: executable,
Args: []string{executable, teleport.CheckHomeDirSubCommand},
Env: []string{"HOME=" + localUser.HomeDir},
SysProcAttr: &syscall.SysProcAttr{
Setsid: true,
Credential: credential,
},
}
// Perform OS-specific tweaks to the command.
reexecCommandOSTweaks(cmd)
if err := cmd.Run(); err != nil {
if cmd.ProcessState.ExitCode() == teleport.HomeDirNotFound {
return false, nil
}
return false, trace.Wrap(err)
}
return true, nil
}
// Spawns a process with the given credentials, outliving the context.
func (o *osWrapper) newParker(ctx context.Context, credential syscall.Credential) error {
executable, err := os.Executable()
if err != nil {
return trace.Wrap(err)
}
cmd := o.CommandContext(ctx, executable, teleport.ParkSubCommand)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &credential,
}
// Perform OS-specific tweaks to the command.
reexecCommandOSTweaks(cmd)
if err := cmd.Start(); err != nil {
return trace.Wrap(err)
}
// the process will get killed when the context ends, but we still need to
// Wait on it
go cmd.Wait()
return nil
}
// getCmdCredentials parses the uid, gid, and groups of the
// given user into a credential object for a command to use.
func getCmdCredential(localUser *user.User) (*syscall.Credential, error) {
uid, err := strconv.Atoi(localUser.Uid)
if err != nil {
return nil, trace.Wrap(err)
}
gid, err := strconv.Atoi(localUser.Gid)
if err != nil {
return nil, trace.Wrap(err)
}
// Lookup supplementary groups for the user.
userGroups, err := localUser.GroupIds()
if err != nil {
return nil, trace.Wrap(err)
}
groups := make([]uint32, 0)
for _, sgid := range userGroups {
igid, err := strconv.Atoi(sgid)
if err != nil {
log.Warnf("Cannot interpret user group: '%v'", sgid)
} else {
groups = append(groups, uint32(igid))
}
}
if len(groups) == 0 {
groups = append(groups, uint32(gid))
}
return &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Groups: groups,
}, nil
}