Refactored launching of shell.

Refactored launching of shell to call PAM first. This allows a PAM
module to create the user and home directory before attempting to launch
a shell for said user.

To do this the command passed to Teleport during re-exec has changed.
Before the Teleport master process would resolve the user fully (UID,
GUID, supplementary groups, shell, home directory) before re-launching
itself to then launch a shell. However, if PAM is used to create the
user on the fly and PAM has not been called yet, this will fail.

Instead that work has now been pushed to occur in the child process.
This means the Teleport master process now creates a payload with the
minimum needed from *srv.ServerContext and will then re-exec itself. The
child process will call PAM and then attempt to resolve the user (UID,
GUID, supplementary groups, shell, home directory).
This commit is contained in:
Russell Jones 2020-01-30 11:36:52 -08:00 committed by Russell Jones
parent 53dfa6fb09
commit f75a80c1f9
10 changed files with 507 additions and 325 deletions

View file

@ -19,6 +19,7 @@ limitations under the License.
package pam
import (
"encoding/json"
"io"
"github.com/gravitational/trace"
@ -33,13 +34,10 @@ type Config struct {
// ServiceName is the name of the policy to apply typically in /etc/pam.d/
ServiceName string
// Username is the name of the target user.
Username string
// Metadata is additional metadata about the user that Teleport stores in
// LoginContext is additional metadata about the user that Teleport stores in
// the PAM_RUSER field. It can be extracted by PAM modules like
// pam_script.so to configure the users environment.
Metadata string
LoginContext *LoginContextV1
// Stdin is the input stream which the conversation function will use to
// obtain data from the user.
@ -59,8 +57,8 @@ func (c *Config) CheckDefaults() error {
if c.ServiceName == "" {
return trace.BadParameter("required parameter ServiceName missing")
}
if c.Username == "" {
return trace.BadParameter("required parameter Username missing")
if c.LoginContext == nil {
return trace.BadParameter("login context required")
}
if c.Stdin == nil {
return trace.BadParameter("required parameter Stdin missing")
@ -74,3 +72,31 @@ func (c *Config) CheckDefaults() error {
return nil
}
// LoginContextV1 is passed to PAM modules in the PAM_RUSER field.
type LoginContextV1 struct {
// Version is the version of this struct.
Version int `json:"version"`
// Username is the Teleport user (identity) that is attempting to login.
Username string `json:"username"`
// Login is the *nix login that that is being used.
Login string `json:"login"`
// Roles is a list of roles assigned to the user.
Roles []string `json:"roles"`
}
// Marshal marshals the login context into a format that can be passed to
// PAM modules.
func (c *LoginContextV1) Marshal() (string, error) {
c.Version = 1
buf, err := json.Marshal(c)
if err != nil {
return "", trace.Wrap(err)
}
return string(buf), nil
}

View file

@ -231,12 +231,12 @@ type PAM struct {
// service_name is the name of the PAM policy to use.
service_name *C.char
// user is the name of the target user.
user *C.char
// login is the name of the host login.
login *C.char
// metadata is additional data about the user that Teleport passes in
// loginContext is additional data about the user that Teleport passes in
// PAM_RUSER.
metadata *C.char
loginContext *C.char
// handlerIndex is the index to the package level handler map.
handlerIndex int
@ -264,7 +264,7 @@ func Open(config *Config) (*PAM, error) {
// C strings. Since the C strings are allocated on the heap in Go code, this
// memory must be released (and will be on the call to the Close method).
p.service_name = C.CString(config.ServiceName)
p.user = C.CString(config.Username)
p.login = C.CString(config.LoginContext.Login)
// C code does not know that this PAM context exists. To ensure the
// conversation function can get messages to the right context, a handle
@ -276,15 +276,19 @@ func Open(config *Config) (*PAM, error) {
// Create and initialize a PAM context. The pam_start function will
// allocate pamh if needed and the pam_end function will release any
// allocated memory.
p.retval = C._pam_start(pamHandle, p.service_name, p.user, p.conv, &p.pamh)
p.retval = C._pam_start(pamHandle, p.service_name, p.login, p.conv, &p.pamh)
if p.retval != C.PAM_SUCCESS {
return nil, p.codeToError(p.retval)
}
// Pack the metadata into the PAM_RUSER field where it can be extracted
// Pack the login context into the PAM_RUSER field where it can be extracted
// by a PAM module.
p.metadata = C.CString(config.Metadata)
p.retval = C._pam_set_item(pamHandle, p.pamh, C.PAM_RUSER, unsafe.Pointer(p.metadata))
buffer, err := config.LoginContext.Marshal()
if err != nil {
return nil, trace.Wrap(err)
}
p.loginContext = C.CString(buffer)
p.retval = C._pam_set_item(pamHandle, p.pamh, C.PAM_RUSER, unsafe.Pointer(p.loginContext))
if p.retval != C.PAM_SUCCESS {
return nil, p.codeToError(p.retval)
}
@ -334,8 +338,8 @@ func (p *PAM) Close() error {
// Release strings that were allocated when opening the PAM context.
C.free(unsafe.Pointer(p.service_name))
C.free(unsafe.Pointer(p.user))
C.free(unsafe.Pointer(p.metadata))
C.free(unsafe.Pointer(p.login))
C.free(unsafe.Pointer(p.loginContext))
return nil
}

View file

@ -38,6 +38,12 @@ func (p *PAM) Close() error {
return nil
}
// Environment returns the PAM environment variables associated with a PAM
// handle.
func (p *PAM) Environment() []string {
return nil
}
// BuildHasPAM returns true if the binary was build with support for PAM
// compiled in.
func BuildHasPAM() bool {

125
lib/pam/pam_test.go Normal file
View file

@ -0,0 +1,125 @@
/*
Copyright 2020 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 pam
import (
"bytes"
"encoding/json"
"fmt"
"os/user"
"testing"
"github.com/gravitational/teleport/lib/utils"
"gopkg.in/check.v1"
)
type Suite struct{}
var _ = fmt.Printf
var _ = check.Suite(&Suite{})
func TestPAM(t *testing.T) { check.TestingT(t) }
func (s *Suite) SetUpSuite(c *check.C) {
utils.InitLoggerForTests()
}
func (s *Suite) TearDownSuite(c *check.C) {}
func (s *Suite) SetUpTest(c *check.C) {}
func (s *Suite) TearDownTest(c *check.C) {}
// TestEcho makes sure that the PAM_RUSER variable passed to a PAM module
// is correctly set
//
// The PAM module used, pam_teleport.so is called from the policy file
// teleport-session-echo-ruser. The policy file instructs pam_teleport.so to
// echo the contents of PAM_RUSER to stdout where this test can read, parse,
// and validate it's output.
func (s *Suite) TestEcho(c *check.C) {
// Skip this test if the binary was not built with PAM support.
if !BuildHasPAM() || !SystemHasPAM() {
c.Skip("Skipping test: PAM support not enabled.")
}
local, err := user.Current()
c.Assert(err, check.IsNil)
var buf bytes.Buffer
_, err = Open(&Config{
Enabled: true,
ServiceName: "teleport-session-echo-ruser",
LoginContext: &LoginContextV1{
Version: 1,
Username: "foo",
Login: local.Username,
Roles: []string{"baz", "qux"},
},
Stdin: &discardReader{},
Stdout: &buf,
Stderr: &buf,
})
c.Assert(err, check.IsNil)
var context LoginContextV1
err = json.Unmarshal(buf.Bytes(), &context)
c.Assert(err, check.IsNil)
c.Assert(context.Username, check.Equals, "foo")
c.Assert(context.Login, check.Equals, local.Username)
c.Assert(context.Roles, check.DeepEquals, []string{"baz", "qux"})
}
// TestEnvironment makes sure that PAM environment variables (environment
// variables set by a PAM module) can be accessed from the PAM handle/context
// in Go code.
//
// The PAM module used, pam_teleport.so is called from the policy file
// teleport-session-environment. The policy file instructs pam_teleport.so to
// read in the first argument and set it as a PAM environment variable. This
// test then validates it matches what was set in the policy file.
func (s *Suite) TestEnvironment(c *check.C) {
// Skip this test if the binary was not built with PAM support.
if !BuildHasPAM() || !SystemHasPAM() {
c.Skip("Skipping test: PAM support not enabled.")
}
local, err := user.Current()
c.Assert(err, check.IsNil)
var buf bytes.Buffer
pamContext, err := Open(&Config{
Enabled: true,
ServiceName: "teleport-session-environment",
LoginContext: &LoginContextV1{
Login: local.Username,
},
Stdin: &discardReader{},
Stdout: &buf,
Stderr: &buf,
})
c.Assert(err, check.IsNil)
c.Assert(pamContext.Environment(), check.HasLen, 1)
c.Assert(pamContext.Environment()[0], check.Equals, "foo=bar")
}
type discardReader struct {
}
func (d *discardReader) Read(p []byte) (int, error) {
return len(p), nil
}

View file

@ -19,8 +19,6 @@ package srv
import (
"fmt"
"net"
"os"
"os/user"
"time"
"golang.org/x/crypto/ssh"
@ -191,17 +189,6 @@ func (h *AuthHandlers) UserKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*s
}
h.Debugf("Successfully authenticated")
// see if the host user is valid, we only do this when logging into a teleport node
if h.isTeleportNode() {
_, err = user.Lookup(conn.User())
if err != nil {
host, _ := os.Hostname()
h.Warnf("host '%s' does not have OS user '%s'", host, conn.User())
h.Errorf("no such user")
return nil, trace.AccessDenied("no such user: '%s'", conn.User())
}
}
clusterName, err := h.AccessPoint.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)

View file

@ -630,6 +630,92 @@ func (c *ServerContext) String() string {
return fmt.Sprintf("ServerContext(%v->%v, user=%v, id=%v)", c.Conn.RemoteAddr(), c.Conn.LocalAddr(), c.Conn.User(), c.id)
}
// ExecCommand takes a *ServerContext and extracts the parts needed to create
// an *execCommand which can be re-sent to Teleport.
func (c *ServerContext) ExecCommand() (*execCommand, error) {
var pamEnabled bool
var pamServiceName string
// If this code is running on a node, check if PAM is enabled or not.
if c.srv.Component() == teleport.ComponentNode {
conf, err := c.srv.GetPAM()
if err != nil {
return nil, trace.Wrap(err)
}
pamEnabled = conf.Enabled
pamServiceName = conf.ServiceName
}
// If the identity has roles, extract the role names.
var roleNames []string
if len(c.Identity.RoleSet) > 0 {
roleNames = c.Identity.RoleSet.RoleNames()
}
// Create the execCommand that will be sent to the child process.
return &execCommand{
Command: c.ExecRequest.GetCommand(),
Username: c.Identity.TeleportUser,
Login: c.Identity.Login,
Roles: roleNames,
Terminal: c.termAllocated || c.ExecRequest.GetCommand() == "",
RequestType: c.request.Type,
PermitUserEnvironment: c.srv.PermitUserEnvironment(),
Environment: buildEnvironment(c),
PAM: pamEnabled,
ServiceName: pamServiceName,
IsTestStub: c.IsTestStub,
}, nil
}
// buildEnvironment constructs a list of environment variables from
// cluster information.
func buildEnvironment(ctx *ServerContext) []string {
var env []string
// Apply environment variables passed in from client.
for k, v := range ctx.env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
// Parse the local and remote addresses to build SSH_CLIENT and
// SSH_CONNECTION environment variables.
remoteHost, remotePort, err := net.SplitHostPort(ctx.Conn.RemoteAddr().String())
if err != nil {
log.Debugf("Failed to split remote address: %v.", err)
} else {
localHost, localPort, err := net.SplitHostPort(ctx.Conn.LocalAddr().String())
if err != nil {
log.Debugf("Failed to split local address: %v.", err)
} else {
env = append(env,
fmt.Sprintf("SSH_CLIENT=%s %s %s", remoteHost, remotePort, localPort),
fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", remoteHost, remotePort, localHost, localPort))
}
}
// If a session has been created try and set TERM, SSH_TTY, and SSH_SESSION_ID.
if ctx.session != nil {
env = append(env, fmt.Sprintf("TERM=%v", ctx.session.term.GetTermType()))
if ctx.session.term != nil {
env = append(env, fmt.Sprintf("SSH_TTY=%s", ctx.session.term.TTY().Name()))
}
if ctx.session.id != "" {
env = append(env, fmt.Sprintf("%s=%s", teleport.SSHSessionID, ctx.session.id))
}
}
// Set some Teleport specific environment variables: SSH_TELEPORT_USER,
// SSH_SESSION_WEBPROXY_ADDR, SSH_TELEPORT_HOST_UUID, and
// SSH_TELEPORT_CLUSTER_NAME.
env = append(env, teleport.SSHSessionWebproxyAddr+"="+ctx.ProxyPublicAddress())
env = append(env, teleport.SSHTeleportHostUUID+"="+ctx.srv.ID())
env = append(env, teleport.SSHTeleportClusterName+"="+ctx.ClusterName)
env = append(env, teleport.SSHTeleportUser+"="+ctx.Identity.TeleportUser)
return env
}
func closeAll(closers ...io.Closer) error {
var errs []error

View file

@ -24,7 +24,6 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"os/user"
@ -57,52 +56,49 @@ const (
)
// execCommand contains the payload to "teleport exec" will will be used to
// construct and execute a exec.Cmd.
// construct and execute a shell.
type execCommand struct {
// Path the the full path to the binary to execute.
Path string `json:"path"`
// Command is the command to execute. If a interactive session is being
// requested, will be empty.
Command string `json:"command"`
// Args is the list of arguments to pass to the command.
Args []string `json:"args"`
// Username is the username associated with the Teleport identity.
Username string `json:"username"`
// Env is a list of environment variables to pass to the command.
Env []string `json:"env"`
// Login is the local *nix account.
Login string `json:"login"`
// Dir is the working/home directory of the command.
Dir string `json:"dir"`
// Roles is the list of Teleport roles assigned to the Teleport identity.
Roles []string `json:"roles"`
// Uid is the UID under which to spawn the command.
Uid uint32 `json:"uid"`
// ClusterName is the name of the Teleport cluster.
ClusterName string `json:"cluster_name"`
// Gid it the GID under which to spawn the command.
Gid uint32 `json:"gid"`
// Groups is the list of supplementary groups.
Groups []uint32 `json:"groups"`
// SetCreds controls if the process credentials will be set.
SetCreds bool `json:"set_creds"`
// Terminal is if a TTY has been allocated for the session.
// Terminal indicates if a TTY has been allocated for the session. This is
// typically set if either an shell was requested or a TTY was explicitly
// allocated for a exec request.
Terminal bool `json:"term"`
// RequestType is the type of request: either "exec" or "shell".
// 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"`
// PAM contains metadata needed to launch a PAM context.
PAM *pamCommand `json:"pam"`
}
// PAM indicates if PAM support was requested by the node.
PAM bool `json:"pam"`
// pamCommand contains the payload to launch a PAM context.
type pamCommand struct {
// Enabled indicates that PAM has been enabled on this host.
Enabled bool `json:"enabled"`
// ServiceName is the name service whose policy will be loaded.
// ServiceName is the name of the PAM service requested if PAM is enabled.
ServiceName string `json:"service_name"`
// Username is the host login.
Username string `json:"username"`
// 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"`
}
// ExecResult is used internally to send the result of a command execution from
@ -290,7 +286,7 @@ func (e *localExec) String() string {
func RunCommand() {
// 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.
// to it instead.
errorWriter := os.Stdout
// Parent sends the command payload in the third file descriptor.
@ -315,17 +311,6 @@ func RunCommand() {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, err)
}
// Start constructing the the exec.Cmd.
cmd := exec.Cmd{
Path: c.Path,
Args: c.Args,
Dir: c.Dir,
Env: c.Env,
SysProcAttr: &syscall.SysProcAttr{
Setsid: true,
},
}
var tty *os.File
var pty *os.File
@ -338,41 +323,14 @@ func RunCommand() {
if pty == nil || tty == nil {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("pty and tty not found"))
}
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
cmd.SysProcAttr.Setctty = true
cmd.SysProcAttr.Ctty = int(tty.Fd())
errorWriter = tty
} else {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
// Only set process credentials if requested. See comment in the
// "prepareCommand" function for more details.
if c.SetCreds {
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: c.Uid,
Gid: c.Gid,
Groups: c.Groups,
}
}
// 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 {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, err)
}
// If PAM is enabled, open a PAM context.
var pamContext *pam.PAM
if c.PAM.Enabled {
// 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.PAM {
// 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.
@ -390,17 +348,37 @@ func RunCommand() {
}
// Open the PAM context.
pamContext, err = pam.Open(&pam.Config{
ServiceName: c.PAM.ServiceName,
Username: c.PAM.Username,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
pamContext, err := pam.Open(&pam.Config{
ServiceName: c.ServiceName,
LoginContext: &pam.LoginContextV1{
Username: c.Username,
Login: c.Login,
Roles: c.Roles,
},
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
})
if err != nil {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, err)
}
defer pamContext.Close()
// Save off any environment variables that come from PAM.
pamEnvironment = pamContext.Environment()
}
// Build the actual command that will launch the shell.
cmd, err := buildCommand(&c, tty, pty, pamEnvironment)
if err != nil {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, 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 {
errorAndExit(errorWriter, teleport.RemoteCommandFailure, err)
}
// Start the command.
@ -447,198 +425,12 @@ func (e *localExec) transformSecureCopy() error {
return nil
}
// prepareCommand prepares a command execution payload.
func prepareCommand(ctx *ServerContext) (*execCommand, error) {
var c execCommand
// Get the login shell for the user (or fallback to the default).
shellPath, err := shell.GetLoginShell(ctx.Identity.Login)
if err != nil {
log.Debugf("Failed to get login shell for %v: %v. Using default: %v.",
ctx.Identity.Login, err, shell.DefaultShell)
}
if ctx.IsTestStub {
shellPath = "/bin/sh"
}
// Save off the SSH request type. This will be used to control where to
// connect std{out,err} based on the request type: "exec" or "shell".
c.RequestType = ctx.request.Type
// If a term was allocated before (this means it was an exec request with an
// PTY explicitly allocated) or no command was given (which means an
// interactive session was requested), then make sure "teleport exec"
// executes through a PTY.
if ctx.termAllocated || ctx.ExecRequest.GetCommand() == "" {
c.Terminal = true
}
// If no command was given, configure a shell to run in 'login' mode.
// Otherwise, execute a command through bash.
if ctx.ExecRequest.GetCommand() == "" {
// Overwrite whatever was in the exec command (probably empty) with the shell.
ctx.ExecRequest.SetCommand(shellPath)
// Set the path to the path of the shell.
c.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
c.Args = []string{"-" + filepath.Base(shellPath)}
} else {
// Execute commands like OpenSSH does:
// https://github.com/openssh/openssh-portable/blob/master/session.c
c.Path = shellPath
c.Args = []string{shellPath, "-c", ctx.ExecRequest.GetCommand()}
}
clusterName, err := ctx.srv.GetAccessPoint().GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
// Lookup the UID and GID for the user.
osUser, err := user.Lookup(ctx.Identity.Login)
if err != nil {
return nil, trace.Wrap(err)
}
uid, err := strconv.Atoi(osUser.Uid)
if err != nil {
return nil, trace.Wrap(err)
}
c.Uid = uint32(uid)
gid, err := strconv.Atoi(osUser.Gid)
if err != nil {
return nil, trace.Wrap(err)
}
c.Gid = uint32(gid)
// Set the home directory for the user.
c.Dir = osUser.HomeDir
// Lookup supplementary groups for the user.
userGroups, err := osUser.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))
}
c.Groups = groups
// 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.
if strconv.Itoa(os.Getuid()) != osUser.Uid || strconv.Itoa(os.Getgid()) != osUser.Gid {
c.SetCreds = true
log.Debugf("Creating process with UID %v, GID: %v, and Groups: %v.",
uid, gid, groups)
} else {
log.Debugf("Credential process with ambient credentials UID %v, GID: %v, Groups: %v.",
uid, gid, groups)
}
// Create environment for user.
c.Env = []string{
"LANG=en_US.UTF-8",
getDefaultEnvPath(osUser.Uid, defaultLoginDefsPath),
"HOME=" + osUser.HomeDir,
"USER=" + ctx.Identity.Login,
"SHELL=" + shellPath,
teleport.SSHTeleportUser + "=" + ctx.Identity.TeleportUser,
teleport.SSHSessionWebproxyAddr + "=" + ctx.ProxyPublicAddress(),
teleport.SSHTeleportHostUUID + "=" + ctx.srv.ID(),
teleport.SSHTeleportClusterName + "=" + clusterName.GetClusterName(),
}
// Apply environment variables passed in from client.
for n, v := range ctx.env {
c.Env = append(c.Env, fmt.Sprintf("%s=%s", n, v))
}
// Apply SSH_* environment variables.
remoteHost, remotePort, err := net.SplitHostPort(ctx.Conn.RemoteAddr().String())
if err != nil {
log.Warn(err)
} else {
localHost, localPort, err := net.SplitHostPort(ctx.Conn.LocalAddr().String())
if err != nil {
log.Warn(err)
} else {
c.Env = append(c.Env,
fmt.Sprintf("SSH_CLIENT=%s %s %s", remoteHost, remotePort, localPort),
fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", remoteHost, remotePort, localHost, localPort))
}
}
if ctx.session != nil {
if ctx.session.term != nil {
c.Env = append(c.Env, fmt.Sprintf("SSH_TTY=%s", ctx.session.term.TTY().Name()))
}
if ctx.session.id != "" {
c.Env = append(c.Env, fmt.Sprintf("%s=%s", teleport.SSHSessionID, ctx.session.id))
}
}
// If a terminal was allocated, set terminal type variable.
if ctx.session != nil {
c.Env = append(c.Env, fmt.Sprintf("TERM=%v", ctx.session.term.GetTermType()))
}
// If the command is being prepared for local execution, check if PAM should
// be called.
if ctx.srv.Component() == teleport.ComponentNode {
conf, err := ctx.srv.GetPAM()
if err != nil {
return nil, trace.Wrap(err)
}
c.PAM = &pamCommand{
Enabled: conf.Enabled,
ServiceName: conf.ServiceName,
Username: ctx.Identity.Login,
}
}
// If the server allows reading in of ~/.tsh/environment read it in
// and pass environment variables along to new session.
if ctx.srv.PermitUserEnvironment() {
filename := filepath.Join(osUser.HomeDir, ".tsh", "environment")
userEnvs, err := utils.ReadEnvironmentFile(filename)
if err != nil {
return nil, trace.Wrap(err)
}
c.Env = append(c.Env, userEnvs...)
}
return &c, nil
}
// configureCommand creates a command fully configured to execute.
// 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) (*exec.Cmd, error) {
var err error
// Create and marshal command to execute.
cmdmsg, err := prepareCommand(ctx)
// Marshal the parts needed from the *ServerContext into a *execCommand.
cmdmsg, err := ctx.ExecCommand()
if err != nil {
return nil, trace.Wrap(err)
}
@ -660,12 +452,12 @@ func configureCommand(ctx *ServerContext) (*exec.Cmd, error) {
// Set to nil so the close in the context doesn't attempt to re-close.
ctx.cmdw = nil
// Re-execute Teleport and pass along the allocated PTY as well as the
// command reader from where Teleport will know how to re-spawn itself.
// Find the Teleport executable and it's directory on disk.
executable, err := os.Executable()
if err != nil {
return nil, trace.Wrap(err)
}
executableDir, _ := filepath.Split(executable)
// Build the list of arguments to have Teleport re-exec itself. The "-d" flag
// is appended if Teleport is running in debug mode.
@ -675,7 +467,7 @@ func configureCommand(ctx *ServerContext) (*exec.Cmd, error) {
return &exec.Cmd{
Path: executable,
Args: args,
Dir: cmdmsg.Dir,
Dir: executableDir,
ExtraFiles: []*os.File{
ctx.cmdr,
ctx.contr,
@ -683,6 +475,152 @@ func configureCommand(ctx *ServerContext) (*exec.Cmd, error) {
}, nil
}
// buildCommand construct a command that will execute the users shell. This
// function is run by Teleport while it's re-executing.
func buildCommand(c *execCommand, tty *os.File, pty *os.File, pamEnvironment []string) (*exec.Cmd, error) {
var cmd exec.Cmd
// Lookup the UID and GID for the user.
localUser, err := user.Lookup(c.Login)
if err != nil {
return nil, trace.Wrap(err)
}
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))
}
// 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...)
// Set the home directory for the user.
cmd.Dir = localUser.HomeDir
// 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,
Ctty: int(tty.Fd()),
}
} else {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
}
// 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.
if strconv.Itoa(os.Getuid()) != localUser.Uid || strconv.Itoa(os.Getgid()) != localUser.Gid {
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Groups: groups,
}
log.Debugf("Creating process with UID %v, GID: %v, and Groups: %v.",
uid, gid, groups)
} else {
log.Debugf("Credential process with ambient credentials UID %v, GID: %v, Groups: %v.",
uid, gid, groups)
}
return &cmd, nil
}
// waitForContinue will wait 10 seconds for the continue signal, if not
// received, it will stop waiting and exit.
func waitForContinue(contfd *os.File) error {

View file

@ -111,7 +111,8 @@ func (s *ExecSuite) SetUpSuite(c *check.C) {
s.usr, _ = user.Current()
s.ctx = &ServerContext{
IsTestStub: true,
IsTestStub: true,
ClusterName: "localhost",
srv: &fakeServer{
accessPoint: s.a,
auditLog: &fakeLog{},
@ -147,19 +148,21 @@ func (s *ExecSuite) TestOSCommandPrep(c *check.C) {
fmt.Sprintf("HOME=%s", s.usr.HomeDir),
fmt.Sprintf("USER=%s", s.usr.Username),
"SHELL=/bin/sh",
"SSH_TELEPORT_USER=galt",
"SSH_CLIENT=10.0.0.5 4817 3022",
"SSH_CONNECTION=10.0.0.5 4817 127.0.0.1 3022",
"TERM=xterm",
fmt.Sprintf("SSH_TTY=%v", s.ctx.session.term.TTY().Name()),
"SSH_SESSION_ID=xxx",
"SSH_SESSION_WEBPROXY_ADDR=<proxyhost>:3080",
"SSH_TELEPORT_HOST_UUID=00000000-0000-0000-0000-000000000000",
"SSH_TELEPORT_CLUSTER_NAME=localhost",
"SSH_CLIENT=10.0.0.5 4817 3022",
"SSH_CONNECTION=10.0.0.5 4817 127.0.0.1 3022",
fmt.Sprintf("SSH_TTY=%v", s.ctx.session.term.TTY().Name()),
"SSH_SESSION_ID=xxx",
"TERM=xterm",
"SSH_TELEPORT_USER=galt",
}
// Empty command (simple shell).
cmd, err := prepareCommand(s.ctx)
execCmd, err := s.ctx.ExecCommand()
c.Assert(err, check.IsNil)
cmd, err := buildCommand(execCmd, nil, nil, nil)
c.Assert(err, check.IsNil)
c.Assert(cmd, check.NotNil)
c.Assert(cmd.Path, check.Equals, "/bin/sh")
@ -168,9 +171,10 @@ func (s *ExecSuite) TestOSCommandPrep(c *check.C) {
c.Assert(cmd.Env, check.DeepEquals, expectedEnv)
// Non-empty command (exec a prog).
s.ctx.IsTestStub = true
s.ctx.ExecRequest.SetCommand("ls -lh /etc")
cmd, err = prepareCommand(s.ctx)
execCmd, err = s.ctx.ExecCommand()
c.Assert(err, check.IsNil)
cmd, err = buildCommand(execCmd, nil, nil, nil)
c.Assert(err, check.IsNil)
c.Assert(cmd, check.NotNil)
c.Assert(cmd.Path, check.Equals, "/bin/sh")
@ -180,7 +184,9 @@ func (s *ExecSuite) TestOSCommandPrep(c *check.C) {
// Command without args.
s.ctx.ExecRequest.SetCommand("top")
cmd, err = prepareCommand(s.ctx)
execCmd, err = s.ctx.ExecCommand()
c.Assert(err, check.IsNil)
cmd, err = buildCommand(execCmd, nil, nil, nil)
c.Assert(err, check.IsNil)
c.Assert(cmd.Path, check.Equals, "/bin/sh")
c.Assert(cmd.Args, check.DeepEquals, []string{"/bin/sh", "-c", "top"})

View file

@ -934,10 +934,14 @@ func (s *Server) handleDirectTCPIPRequest(wconn net.Conn, sconn *ssh.ServerConn,
// the users screen during port forwarding.
pamContext, err = pam.Open(&pam.Config{
ServiceName: s.pamConfig.ServiceName,
Username: ctx.Identity.Login,
Stdin: ch,
Stderr: ioutil.Discard,
Stdout: ioutil.Discard,
LoginContext: &pam.LoginContextV1{
Username: ctx.Identity.TeleportUser,
Login: ctx.Identity.Login,
Roles: ctx.Identity.RoleSet.RoleNames(),
},
Stdin: ch,
Stderr: ioutil.Discard,
Stdout: ioutil.Discard,
})
if err != nil {
ctx.Errorf("Unable to open PAM context for direct-tcpip request: %v.", err)

View file

@ -640,7 +640,7 @@ func (s *session) startInteractive(ch ssh.Channel, ctx *ServerContext) error {
}
if err := s.term.Run(); err != nil {
ctx.Errorf("Unable to run shell command (%v): %v", ctx.ExecRequest.GetCommand(), err)
ctx.Errorf("Unable to run shell command: %v.", err)
return trace.ConvertSystemError(err)
}
if err := s.addParty(p); err != nil {