mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
session-helper: embeddable binary and embedding logic (#65784)
* Clean up unneeded forwarder for reexec.IsReexec * Disallow reexec through common.Run * Remove the checkhomedir SSH subsystem * Add true reexec subcommand * Assume /proc/self/exe is always usable * Add sessionhelper embed logic
This commit is contained in:
@@ -962,10 +962,6 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// GetHomeDirSubsystem is an SSH subsystem request that Teleport
|
||||
// uses to get the home directory of a remote user.
|
||||
GetHomeDirSubsystem = "gethomedir"
|
||||
|
||||
// SFTPSubsystem is the SFTP SSH subsystem.
|
||||
SFTPSubsystem = "sftp"
|
||||
)
|
||||
|
||||
@@ -176,6 +176,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/keys-pub/go-libfido2 v1.5.3-0.20220306005615-8ab03fb1ec27
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/mailgun/mailgun-go/v4 v4.23.0
|
||||
github.com/mark3labs/mcp-go v0.43.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
@@ -468,7 +469,6 @@ require (
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||
github.com/klauspost/asmfmt v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
|
||||
@@ -195,6 +195,7 @@ import (
|
||||
webapp "github.com/gravitational/teleport/lib/web/app"
|
||||
"github.com/gravitational/teleport/session/auditd"
|
||||
"github.com/gravitational/teleport/session/pam"
|
||||
"github.com/gravitational/teleport/session/reexec"
|
||||
"github.com/gravitational/teleport/session/selinux"
|
||||
)
|
||||
|
||||
@@ -3536,6 +3537,15 @@ func (process *TeleportProcess) initSSH() error {
|
||||
logger.WarnContext(process.ExitContext(), warn)
|
||||
}
|
||||
|
||||
// TODO(espadolini): relax this once the selinux module is updated to support the potentially embedded reexec helper
|
||||
if !cfg.SSH.EnableSELinux {
|
||||
checkEmbeddedReexecAndLog(process.ExitContext(), logger)
|
||||
} else {
|
||||
logger.DebugContext(process.ExitContext(),
|
||||
"The embedded session helper is not supported when SELinux support is enabled.",
|
||||
)
|
||||
}
|
||||
|
||||
useLocalListener := cfg.SSH.ForceListen || !conn.UseTunnel()
|
||||
|
||||
// Provide helpful log message if listen_addr or public_addr are not being
|
||||
@@ -3828,6 +3838,23 @@ func (process *TeleportProcess) initSSH() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEmbeddedReexecAndLog(ctx context.Context, logger *slog.Logger) {
|
||||
if ok, err := reexec.InitEmbeddedReexec(); err != nil {
|
||||
logger.WarnContext(ctx,
|
||||
"This Teleport build supports the embedded session helper but it is not available in this environment, performance of user sessions might be impacted.",
|
||||
"error", err,
|
||||
)
|
||||
} else if ok {
|
||||
logger.DebugContext(ctx,
|
||||
"The embedded session helper is available and will be used for user sessions.",
|
||||
)
|
||||
} else {
|
||||
logger.DebugContext(ctx,
|
||||
"This Teleport build does not support the embedded session helper for user sessions.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterWithAuthServer uses one time provisioning token obtained earlier
|
||||
// from the server to get a pair of SSH keys signed by Auth server host
|
||||
// certificate authority
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package regular
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/gravitational/teleport/lib/srv"
|
||||
"github.com/gravitational/teleport/session/reexec"
|
||||
)
|
||||
|
||||
type homeDirSubsys struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newHomeDirSubsys() *homeDirSubsys {
|
||||
return &homeDirSubsys{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *homeDirSubsys) Start(_ context.Context, serverConn *ssh.ServerConn, ch ssh.Channel, _ *ssh.Request, _ *srv.ServerContext) error {
|
||||
defer close(h.done)
|
||||
|
||||
connUser := serverConn.User()
|
||||
localUser, err := user.Lookup(connUser)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
exists, err := reexec.CheckHomeDir(localUser)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
homeDir := localUser.HomeDir
|
||||
if !exists {
|
||||
homeDir = string(os.PathSeparator)
|
||||
}
|
||||
_, err = io.Copy(ch, strings.NewReader(homeDir))
|
||||
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (h *homeDirSubsys) Wait() error {
|
||||
<-h.done
|
||||
return nil
|
||||
}
|
||||
@@ -2545,9 +2545,6 @@ func (s *Server) parseSubsystemRequest(ctx context.Context, req *ssh.Request, se
|
||||
}
|
||||
|
||||
switch r.Name {
|
||||
// DELETE IN 15.0.0 (deprecated, tsh will not be using this anymore)
|
||||
case teleport.GetHomeDirSubsystem:
|
||||
return newHomeDirSubsys(), nil
|
||||
case teleport.SFTPSubsystem:
|
||||
err := serverContext.CheckSFTPAllowed(s.reg)
|
||||
if err != nil {
|
||||
|
||||
@@ -2757,9 +2757,9 @@ func TestParseSubsystemRequest(t *testing.T) {
|
||||
wantErrInRegularMode: false,
|
||||
},
|
||||
{
|
||||
name: teleport.GetHomeDirSubsystem,
|
||||
name: "gethomedir",
|
||||
wantErrInProxyMode: true,
|
||||
wantErrInRegularMode: false,
|
||||
wantErrInRegularMode: true,
|
||||
},
|
||||
{
|
||||
name: "proxysites",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//go:build !sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gravitational/teleport/session/reexec"
|
||||
"github.com/gravitational/teleport/session/reexec/reexecconstants"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "Failed to launch: missing subcommand.\n")
|
||||
os.Exit(reexecconstants.RemoteCommandFailure)
|
||||
}
|
||||
|
||||
reexec.RunAndExit(os.Args[1])
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/sessionhelper_*.gz
|
||||
@@ -0,0 +1,28 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
// InitEmbeddedReexec tries to set up the embedded session helper for execution,
|
||||
// returning true if the helper is available and was tested successfully, false
|
||||
// if there is no embedded session helper in the build, or an error if the
|
||||
// helper is present in the build but the setup failed. This function can be
|
||||
// called multiple times with no adverse effects, and, if successful, any
|
||||
// subsequent call to [CommandOSTweaks] will result in a reexecution using the
|
||||
// embedded helper rather than the current process.
|
||||
func InitEmbeddedReexec() (bool, error) {
|
||||
return initEmbeddedReexec()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
var embeddedReexecMemfd atomic.Pointer[os.File]
|
||||
|
||||
var embeddedReexecOnce sync.Once
|
||||
var embeddedReexecError error
|
||||
|
||||
func initEmbeddedReexec() (bool, error) {
|
||||
embeddedReexecOnce.Do(func() {
|
||||
if os.Getenv("TELEPORT_UNSTABLE_DISABLE_EMBEDDED_REEXEC") == "yes" {
|
||||
embeddedReexecError = trace.Errorf("embedded binary disabled by TELEPORT_UNSTABLE_DISABLE_EMBEDDED_REEXEC")
|
||||
return
|
||||
}
|
||||
|
||||
// this name is only displayed as a link in /proc/<pid>/exe or /proc/<pid>/fd/<n>
|
||||
mf, err := loadEmbeddedReexec("teleport-sessionhelper", sessionHelperGZ)
|
||||
if err != nil {
|
||||
embeddedReexecError = trace.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
embeddedReexecMemfd.Store(mf)
|
||||
})
|
||||
|
||||
return true, embeddedReexecError
|
||||
}
|
||||
|
||||
func setLinuxReexecPath(cmd *exec.Cmd) {
|
||||
if cmd.SysProcAttr != nil && cmd.SysProcAttr.Credential != nil {
|
||||
// this should never happen because the main process always launches the
|
||||
// "exec" or "networking" subcommands as itself and there's no other
|
||||
// direct launches with unprivileged credentials anymore, but if we
|
||||
// accidentally launched the session helper directly as an unprivileged
|
||||
// child it just wouldn't work due to permissions, because the child
|
||||
// wouldn't be able to launch "/proc/<pid of parent>/fd/<n>" and there's
|
||||
// no other way to pass a file descriptor such that it's available in
|
||||
// the post-fork pre-exec environment but not inherited by the child in
|
||||
// go
|
||||
cmd.Path = "/proc/self/exe"
|
||||
return
|
||||
}
|
||||
if mf := embeddedReexecMemfd.Load(); mf != nil {
|
||||
cmd.Path = mf.Name()
|
||||
return
|
||||
}
|
||||
// we failed to initialize the reexec memfd or we haven't bothered to call
|
||||
// [initEmbeddedReexec] in this process (maybe because we're reexecuting a
|
||||
// subcommand from a subcommand in the main binary)
|
||||
cmd.Path = "/proc/self/exe"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed sessionhelper_linux_386.gz
|
||||
var sessionHelperGZ string
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed sessionhelper_linux_amd64.gz
|
||||
var sessionHelperGZ string
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed sessionhelper_linux_arm.gz
|
||||
var sessionHelperGZ string
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed sessionhelper_linux_arm64.gz
|
||||
var sessionHelperGZ string
|
||||
@@ -0,0 +1,112 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/gravitational/teleport/session/reexec/reexecconstants"
|
||||
)
|
||||
|
||||
// loadEmbeddedReexec loads the given gzipped session helper binary into a
|
||||
// sealed, executable memfd and tests it to confirm that it can be executed,
|
||||
// then returns the memfd.
|
||||
func loadEmbeddedReexec(name string, dataGZ string) (*os.File, error) {
|
||||
mf, err := createExecutableMemfd(name, dataGZ)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// TODO(espadolini): set the selinux file label on mf, figure out if it
|
||||
// should be the same as the current executable or a fixed
|
||||
// teleport_ssh_exec_t
|
||||
|
||||
// run a self test with a noop command, just to be sure
|
||||
c := &exec.Cmd{
|
||||
Path: mf.Name(),
|
||||
Args: []string{"teleport", reexecconstants.TrueSubCommand},
|
||||
}
|
||||
if err := c.Run(); err != nil {
|
||||
_ = mf.Close()
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "failed to launch embedded binary")
|
||||
}
|
||||
|
||||
return mf, nil
|
||||
}
|
||||
|
||||
// createExecutableMemfd returns a sealed memfd with the given name, containing
|
||||
// the uncompressed gzipped binary.
|
||||
func createExecutableMemfd(name string, dataGZ string) (_ *os.File, err error) {
|
||||
// we follow the logic from runc here
|
||||
// https://github.com/opencontainers/runc/blob/edbed618bff99edaaa9d622f9f8687004fe5e50a/libcontainer/system/linux.go#L89
|
||||
mfd, err := unix.MemfdCreate(name, unix.MFD_ALLOW_SEALING|unix.MFD_CLOEXEC|unix.MFD_EXEC)
|
||||
if errors.Is(err, unix.EINVAL) {
|
||||
mfd, err = unix.MemfdCreate(name, unix.MFD_ALLOW_SEALING|unix.MFD_CLOEXEC)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EACCES) {
|
||||
return nil, trace.AccessDenied("failed to create executable memfd for embedded binary (possibly due to vm.memfd_noexec=2)")
|
||||
|
||||
}
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "failed to create executable memfd for embedded binary")
|
||||
}
|
||||
|
||||
// This must not be "/proc/self/fd/<n>" because in the post-fork, pre-exec
|
||||
// environment of the child the file descriptors inherited from the parent
|
||||
// are not guaranteed to be available - the fd will likely end up being
|
||||
// small if this runs early, so it will likely get overridden as part of the
|
||||
// exec machinery with one of the files in [exec.Cmd.ExtraFiles]. This does
|
||||
// however mean that an unprivileged child will not be able to launch the
|
||||
// helper, so this is only usable when launching it as the same user that
|
||||
// the Teleport process is running as. [setLinuxReexecPath] will ensure that
|
||||
// that's the case before attempting to use the memfd, and it will fall back
|
||||
// to a regular reexecution of "/proc/self/exe" if need be.
|
||||
mf := os.NewFile(uintptr(mfd), fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), mfd))
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = mf.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
gzreader, err := gzip.NewReader(strings.NewReader(dataGZ))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to decompress embedded binary (this is a bug)")
|
||||
}
|
||||
if _, err := gzreader.WriteTo(mf); err != nil {
|
||||
return nil, trace.Wrap(err, "failed to write embedded binary in memfd")
|
||||
}
|
||||
if err := mf.Chmod(0o555); err != nil {
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "failed to chmod memfd for embedded binary")
|
||||
}
|
||||
|
||||
// ignore errors here since F_SEAL_EXEC is newer than the rest of the memfd interface
|
||||
_, _ = unix.FcntlInt(uintptr(mfd), unix.F_ADD_SEALS, unix.F_SEAL_EXEC)
|
||||
|
||||
if _, err := unix.FcntlInt(uintptr(mfd), unix.F_ADD_SEALS, unix.F_SEAL_SEAL|unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE); err != nil {
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "failed to seal memfd for embedded binary")
|
||||
}
|
||||
|
||||
return mf, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestLoadEmbeddedReexec(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
truePath, err := exec.LookPath("true")
|
||||
require.NoError(t, err)
|
||||
|
||||
trueBin, err := os.ReadFile(truePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := new(strings.Builder)
|
||||
gzwriter := gzip.NewWriter(b)
|
||||
_, err = gzwriter.Write(trueBin)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gzwriter.Close())
|
||||
|
||||
trueGZ := b.String()
|
||||
|
||||
mf, err := loadEmbeddedReexec("true", trueGZ)
|
||||
require.NoError(t, err)
|
||||
defer mf.Close()
|
||||
|
||||
r, err := unix.FcntlInt(mf.Fd(), unix.F_GET_SEALS, 0)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, r&unix.F_SEAL_SEAL)
|
||||
require.NotZero(t, r&unix.F_SEAL_SHRINK)
|
||||
require.NotZero(t, r&unix.F_SEAL_GROW)
|
||||
require.NotZero(t, r&unix.F_SEAL_WRITE)
|
||||
|
||||
if r&unix.F_SEAL_EXEC == 0 {
|
||||
t.Log("didn't get F_SEAL_EXEC on memfd, ignoring since the kernel might be old")
|
||||
}
|
||||
|
||||
fi, err := mf.Stat()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fs.FileMode(0o555), fi.Mode())
|
||||
|
||||
// loadEmbeddedReexec should've already tested it, but let's be thorough
|
||||
err = (&exec.Cmd{
|
||||
Path: mf.Name(),
|
||||
Args: []string{"true"},
|
||||
}).Run()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
@@ -14,13 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package srv
|
||||
package reexec
|
||||
|
||||
import (
|
||||
"github.com/gravitational/teleport/session/reexec"
|
||||
)
|
||||
|
||||
//go:fix inline
|
||||
func IsReexec() bool {
|
||||
return reexec.IsReexec()
|
||||
func initEmbeddedReexec() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build !sessionhelper_embed
|
||||
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package reexec
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func setLinuxReexecPath(cmd *exec.Cmd) {
|
||||
cmd.Path = "/proc/self/exe"
|
||||
}
|
||||
@@ -826,7 +826,7 @@ func RunNetworking() (code int, err error) {
|
||||
// Create a minimal default environment for the user.
|
||||
workingDir := rootDirectory
|
||||
|
||||
hasAccess, err := CheckHomeDir(localUser)
|
||||
hasAccess, err := checkHomeDir(localUser)
|
||||
if hasAccess && err == nil {
|
||||
workingDir = localUser.HomeDir
|
||||
}
|
||||
@@ -1123,6 +1123,8 @@ func RunAndExit(commandType string) {
|
||||
code = runCheckHomeDir()
|
||||
case reexecconstants.ParkSubCommand:
|
||||
code = runPark()
|
||||
case reexecconstants.TrueSubCommand:
|
||||
// nothing to do
|
||||
case reexecconstants.SFTPSubCommand:
|
||||
initLogger("sftp", os.Stderr, ExecLogConfig{})
|
||||
err = reexecsftp.RunSFTP(slog.Default())
|
||||
@@ -1176,6 +1178,7 @@ func IsReexec() bool {
|
||||
reexecconstants.NetworkingSubCommand,
|
||||
reexecconstants.CheckHomeDirSubCommand,
|
||||
reexecconstants.ParkSubCommand,
|
||||
reexecconstants.TrueSubCommand,
|
||||
reexecconstants.SFTPSubCommand:
|
||||
return true
|
||||
default:
|
||||
@@ -1356,7 +1359,7 @@ func BuildCommand(c *ExecCommand, localUser *user.User, pamEnvironment []string)
|
||||
// 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.
|
||||
hasAccess, err := CheckHomeDir(localUser)
|
||||
hasAccess, err := checkHomeDir(localUser)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -1457,10 +1460,10 @@ func hasAccessibleHomeDir() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckHomeDir checks if the user's home directory exists and is accessible to the user. Only catastrophic
|
||||
// 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) {
|
||||
func checkHomeDir(localUser *user.User) (bool, error) {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
@@ -26,69 +24,33 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// errors in open/openat are signaled by returning -1, we don't really care
|
||||
// about the specifics anyway, so we can just ignore the error value
|
||||
//
|
||||
// we're opening with O_PATH rather than O_RDONLY because the binary might
|
||||
// not actually be readable (but only executable)
|
||||
fd1, _ := syscall.Open("/proc/self/exe", unix.O_PATH|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
|
||||
fd2, _ := syscall.Open("/proc/self/exe", unix.O_PATH|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
|
||||
|
||||
// this can happen if both calls failed (returning -1) or if we're
|
||||
// running in a version of qemu-user that's affected by this bug:
|
||||
// https://gitlab.com/qemu-project/qemu/-/issues/927
|
||||
// (hopefully they'll also add special handling for execve on /proc/self/exe
|
||||
// if they ever fix that bug)
|
||||
if fd1 == fd2 {
|
||||
return
|
||||
}
|
||||
|
||||
// closing -1 is harmless, no need to check here
|
||||
syscall.Close(fd1)
|
||||
syscall.Close(fd2)
|
||||
|
||||
// if one Open has failed but not the other we can't really
|
||||
// trust the availability of "/proc/self/exe"
|
||||
if fd1 == -1 || fd2 == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
reexecPath = "/proc/self/exe"
|
||||
}
|
||||
|
||||
// reexecPath specifies a path to execute on reexec, overriding Path in the cmd
|
||||
// passed to reexecCommandOSTweaks, if not empty.
|
||||
var reexecPath string
|
||||
|
||||
func CommandOSTweaks(cmd *exec.Cmd) {
|
||||
setLinuxReexecPath(cmd)
|
||||
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = new(syscall.SysProcAttr)
|
||||
}
|
||||
|
||||
// Linux only: when parent process (node) dies unexpectedly without
|
||||
// cleaning up child processes, send a signal for graceful shutdown
|
||||
// to children.
|
||||
cmd.SysProcAttr.Pdeathsig = syscall.SIGQUIT
|
||||
|
||||
// replace the path on disk (which might not exist, or refer to an
|
||||
// upgraded version of teleport) with reexecPath, which contains
|
||||
// some path that refers to the specific binary we're running
|
||||
if reexecPath != "" {
|
||||
cmd.Path = reexecPath
|
||||
}
|
||||
}
|
||||
|
||||
// if we ever need to run parkers on macOS or other platforms with no PDEATHSIG
|
||||
// we should rework the parker to block on a pipe so it can exit when its parent
|
||||
// is terminated
|
||||
func parkerCommandOSTweaks(cmd *exec.Cmd) {
|
||||
CommandOSTweaks(cmd)
|
||||
setLinuxReexecPath(cmd)
|
||||
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = new(syscall.SysProcAttr)
|
||||
}
|
||||
|
||||
// parker processes can leak if their PDEATHSIG is SIGQUIT, otherwise we
|
||||
// could just use reexecCommandOSTweaks
|
||||
// could just use [commandOSTweaks]
|
||||
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
|
||||
}
|
||||
|
||||
@@ -96,6 +58,7 @@ func userCommandOSTweaks(cmd *exec.Cmd) {
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = new(syscall.SysProcAttr)
|
||||
}
|
||||
|
||||
// Linux only: when parent process (this process) dies unexpectedly, kill
|
||||
// the child process instead of orphaning it.
|
||||
// SIGKILL because we don't control the child process, and it could choose
|
||||
|
||||
@@ -222,22 +222,22 @@ func TestRootCheckHomeDir(t *testing.T) {
|
||||
require.NoError(t, os.Chown(home, uid, gid))
|
||||
require.NoError(t, os.Chown(file, uid, gid))
|
||||
|
||||
hasAccess, err := CheckHomeDir(testUser)
|
||||
hasAccess, err := checkHomeDir(testUser)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hasAccess)
|
||||
|
||||
changeHomeDir(t, login, file)
|
||||
hasAccess, err = CheckHomeDir(testUser)
|
||||
hasAccess, err = checkHomeDir(testUser)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasAccess)
|
||||
|
||||
changeHomeDir(t, login, notFound)
|
||||
hasAccess, err = CheckHomeDir(testUser)
|
||||
hasAccess, err = checkHomeDir(testUser)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasAccess)
|
||||
|
||||
changeHomeDir(t, login, noAccess)
|
||||
hasAccess, err = CheckHomeDir(testUser)
|
||||
hasAccess, err = checkHomeDir(testUser)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hasAccess)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ const (
|
||||
// spawning the intended child process.
|
||||
ParkSubCommand = "park"
|
||||
|
||||
// TrueSubCommand is a sub-command that exits with a success exit code.
|
||||
TrueSubCommand = "true"
|
||||
|
||||
// SFTPSubCommand is the sub-command Teleport uses to re-exec itself to
|
||||
// handle SFTP connections.
|
||||
SFTPSubCommand = "sftp"
|
||||
|
||||
@@ -57,7 +57,6 @@ import (
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||||
"github.com/gravitational/teleport/lib/versioncontrol"
|
||||
"github.com/gravitational/teleport/session/reexec"
|
||||
"github.com/gravitational/teleport/session/reexec/reexecconstants"
|
||||
"github.com/gravitational/teleport/session/selinux"
|
||||
)
|
||||
@@ -115,7 +114,8 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
|
||||
exec := app.Command(reexecconstants.ExecSubCommand, "Used internally by Teleport to re-exec itself to run a command.").Hidden()
|
||||
networking := app.Command(reexecconstants.NetworkingSubCommand, "Used internally by Teleport to re-exec itself to handle networking requests.").Hidden()
|
||||
checkHomeDir := app.Command(reexecconstants.CheckHomeDirSubCommand, "Used internally by Teleport to re-exec itself to check access to a directory.").Hidden()
|
||||
park := app.Command(reexecconstants.ParkSubCommand, "Used internally by Teleport to re-exec itself to do nothing.").Hidden()
|
||||
park := app.Command(reexecconstants.ParkSubCommand, "Used internally by Teleport to re-exec itself to do nothing, forever.").Hidden()
|
||||
trueCmd := app.Command(reexecconstants.TrueSubCommand, "Used internally by Teleport to re-exec itself to do nothing, successfully.").Hidden()
|
||||
app.HelpFlag.Short('h')
|
||||
|
||||
// define start flags:
|
||||
@@ -746,20 +746,13 @@ Examples:
|
||||
dumpFlags.Roles = defaults.RoleNode
|
||||
err = onConfigDump(dumpFlags)
|
||||
|
||||
// TODO(espadolini): replace these after enterprise calls reexec.MaybeReexec
|
||||
// in main with an error message ("invalid format for reexec subcommand")
|
||||
// because if we got here it's because MaybeReexec didn't find the correct
|
||||
// first argument
|
||||
case exec.FullCommand():
|
||||
reexec.RunAndExit(reexecconstants.ExecSubCommand)
|
||||
case networking.FullCommand():
|
||||
reexec.RunAndExit(reexecconstants.NetworkingSubCommand)
|
||||
case checkHomeDir.FullCommand():
|
||||
reexec.RunAndExit(reexecconstants.CheckHomeDirSubCommand)
|
||||
case park.FullCommand():
|
||||
reexec.RunAndExit(reexecconstants.ParkSubCommand)
|
||||
case sftp.FullCommand():
|
||||
reexec.RunAndExit(reexecconstants.SFTPSubCommand)
|
||||
case exec.FullCommand(),
|
||||
networking.FullCommand(),
|
||||
checkHomeDir.FullCommand(),
|
||||
park.FullCommand(),
|
||||
trueCmd.FullCommand(),
|
||||
sftp.FullCommand():
|
||||
err = trace.BadParameter("invalid command line format for internal reexecution command (this is a bug)")
|
||||
|
||||
case waitNoResolveCmd.FullCommand():
|
||||
err = onWaitNoResolve(waitFlags)
|
||||
|
||||
Reference in New Issue
Block a user