diff --git a/.golangci.yml b/.golangci.yml index 36e1fd50f04..59e4dbb81cc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -393,6 +393,9 @@ linters: - pattern: ^ssh\.(Dial|NewClient|NewClientConn)$ pkg: ^golang\.org/x/crypto/ssh$ msg: Use api/ssh package to construct SSH clients with proper defaults + - pkg: '^os/user$' + pattern: '^user\.(Lookup|LookupId|LookupGroup|LookupGroupId|Current|User\.GroupIds)$' + msg: 'os/user lookup APIs potentially unsafe; use the session/host/user wrapper instead' - pattern: ^ssh\.(Dial|NewClientWithTimeout|NewClientConnWithTimeout)$ pkg: ^github\.com/gravitational/teleport/api/observability/tracing/ssh$ msg: Use api/ssh package to construct SSH clients with proper defaults @@ -567,6 +570,12 @@ linters: path: ^api/ssh/ text: tracessh.NewClientConnWithTimeout + # Allow session/host/user and tests to use os/user functions directly. + - path: '(_test\.go$|^api/|^integration/helpers/|^lib/utils/testutils|^session/host/user/)' + text: "session/host/user" + linters: + - forbidigo + # TODO: Fix forced type assertions in the rest of the codebase and remove this exclusion so that the linter can # enforce this rule everywhere. - linters: diff --git a/lib/client/api.go b/lib/client/api.go index a0170b98183..e6ff087edf0 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -32,7 +32,6 @@ import ( "net" "net/url" "os" - "os/user" "path/filepath" "runtime" "slices" @@ -103,6 +102,7 @@ import ( "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/utils/set" + hostuser "github.com/gravitational/teleport/session/host/user" ) const ( @@ -5012,7 +5012,7 @@ func loopbackPool(proxyAddr string) *x509.CertPool { // Username returns the current user's username func Username() (string, error) { - u, err := user.Current() + u, err := hostuser.Current() if err != nil { return "", trace.Wrap(err) } diff --git a/lib/client/db/mysql/optionfile.go b/lib/client/db/mysql/optionfile.go index 02716ffa2f2..eefea41bc4e 100644 --- a/lib/client/db/mysql/optionfile.go +++ b/lib/client/db/mysql/optionfile.go @@ -20,7 +20,6 @@ package mysql import ( "os" - "os/user" "path/filepath" "strconv" "strings" @@ -29,6 +28,7 @@ import ( "gopkg.in/ini.v1" "github.com/gravitational/teleport/lib/client/db/profile" + hostuser "github.com/gravitational/teleport/session/host/user" ) func init() { @@ -51,7 +51,7 @@ type OptionFile struct { func DefaultConfigPath() (string, error) { home, err := os.UserHomeDir() if err != nil || home == "" { - usr, err := user.Current() + usr, err := hostuser.Current() if err != nil { return "", trace.ConvertSystemError(err) } diff --git a/lib/client/db/postgres/servicefile.go b/lib/client/db/postgres/servicefile.go index 369b0a54cc1..f4a3077f96d 100644 --- a/lib/client/db/postgres/servicefile.go +++ b/lib/client/db/postgres/servicefile.go @@ -20,7 +20,6 @@ package postgres import ( "os" - "os/user" "path/filepath" "strconv" "strings" @@ -29,6 +28,7 @@ import ( "gopkg.in/ini.v1" "github.com/gravitational/teleport/lib/client/db/profile" + hostuser "github.com/gravitational/teleport/session/host/user" ) func init() { @@ -52,7 +52,7 @@ func defaultConfigPath() (string, error) { // TODO(r0mant): Check PGSERVICEFILE and PGSYSCONFDIR env vars as well. home, err := os.UserHomeDir() if err != nil || home == "" { - usr, err := user.Current() + usr, err := hostuser.Current() if err != nil { return "", trace.ConvertSystemError(err) } diff --git a/lib/devicetrust/native/device_darwin.go b/lib/devicetrust/native/device_darwin.go index e35620abd2b..d2a9b290571 100644 --- a/lib/devicetrust/native/device_darwin.go +++ b/lib/devicetrust/native/device_darwin.go @@ -35,7 +35,6 @@ import ( "io/fs" "log/slog" "os/exec" - "os/user" "strings" "sync" "unsafe" @@ -46,6 +45,7 @@ import ( devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/darwin" + hostuser "github.com/gravitational/teleport/session/host/user" ) func enrollDeviceInit() (*devicepb.EnrollDeviceInit, error) { @@ -115,7 +115,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) return nil, trace.Wrap(statusErrorFromC(res)) } - osUser, err := user.Current() + osUser, err := hostuser.Current() if err != nil { return nil, trace.Wrap(err, "reading current user") } diff --git a/lib/devicetrust/native/device_linux.go b/lib/devicetrust/native/device_linux.go index ef00fbc94c5..e12b14cfacc 100644 --- a/lib/devicetrust/native/device_linux.go +++ b/lib/devicetrust/native/device_linux.go @@ -29,7 +29,6 @@ import ( "log/slog" "os" "os/exec" - "os/user" "time" "github.com/google/go-attestation/attest" @@ -39,6 +38,7 @@ import ( "github.com/gravitational/teleport" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/linux" + hostuser "github.com/gravitational/teleport/session/host/user" ) // deviceStateFolderName starts without a "." on Linux systems. @@ -167,7 +167,7 @@ func collectDeviceData(mode CollectDataMode) (*devicepb.DeviceCollectedData, err baseBoardSerialNumber = dmiInfo.BoardSerial } - u, err := user.Current() + u, err := hostuser.Current() if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/integrations/azureoidc/private.go b/lib/integrations/azureoidc/private.go index 932bc5c61d6..7594f3adf6b 100644 --- a/lib/integrations/azureoidc/private.go +++ b/lib/integrations/azureoidc/private.go @@ -25,7 +25,6 @@ import ( "net/http" "net/url" "os" - "os/user" "path" "path/filepath" "strings" @@ -34,6 +33,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/defaults" + hostuser "github.com/gravitational/teleport/session/host/user" ) type msalTokenCache struct { @@ -51,7 +51,7 @@ type exchangeResponse struct { // getRefreshTokens returns all current refresh tokens from the Azure CLI token cache. func getRefreshTokens() ([]msalToken, error) { - usr, err := user.Current() + usr, err := hostuser.Current() if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/integrations/azureoidc/provision.go b/lib/integrations/azureoidc/provision.go index 7cecf1f5889..e7487770da1 100644 --- a/lib/integrations/azureoidc/provision.go +++ b/lib/integrations/azureoidc/provision.go @@ -23,13 +23,13 @@ import ( "fmt" "os" "os/exec" - "os/user" "path/filepath" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/msgraph" + hostuser "github.com/gravitational/teleport/session/host/user" ) // createGraphClient creates a new graph client from ambient credentials (Azure CLI credentials cache). @@ -60,7 +60,7 @@ func EnsureAZLogin(ctx context.Context) error { } func getAzureDir() (string, error) { - usr, err := user.Current() + usr, err := hostuser.Current() if err != nil { return "", trace.Wrap(err) } diff --git a/lib/secretsscanner/authorizedkeys/authorized_keys.go b/lib/secretsscanner/authorizedkeys/authorized_keys.go index ae7f5736462..fc90ec72b3c 100644 --- a/lib/secretsscanner/authorizedkeys/authorized_keys.go +++ b/lib/secretsscanner/authorizedkeys/authorized_keys.go @@ -43,11 +43,7 @@ import ( clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1" "github.com/gravitational/teleport/api/types/accessgraph" "github.com/gravitational/teleport/api/utils/retryutils" -) - -var ( - // ErrUnsupportedPlatform is returned when the operating system is not supported. - ErrUnsupportedPlatform = errors.New("unsupported platform") + hostuser "github.com/gravitational/teleport/session/host/user" ) // Watcher watches for changes to authorized_keys files @@ -96,7 +92,7 @@ func NewWatcher(ctx context.Context, config WatcherConfig) (*Watcher, error) { switch platform := getOS(config); platform { case constants.LinuxOS, constants.DarwinOS: default: - return nil, trace.Wrap(ErrUnsupportedPlatform) + return nil, trace.Wrap(hostuser.ErrUnsupportedPlatform) } if config.HostID == "" { @@ -112,7 +108,7 @@ func NewWatcher(ctx context.Context, config WatcherConfig) (*Watcher, error) { config.Clock = clockwork.NewRealClock() } if config.getHostUsers == nil { - config.getHostUsers = getHostUsers + config.getHostUsers = hostuser.GetHostUsers } w := &Watcher{ diff --git a/lib/secretsscanner/authorizedkeys/users_list_linux.go b/lib/secretsscanner/authorizedkeys/users_list_linux.go deleted file mode 100644 index 9e750d21238..00000000000 --- a/lib/secretsscanner/authorizedkeys/users_list_linux.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 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 . - */ - -package authorizedkeys - -/* -#define _XOPEN_SOURCE 500 -#include -*/ -import "C" - -import ( - "os/user" - "strconv" - "sync" - - "github.com/gravitational/trace" -) - -// pwentLock should be acquired when using MT-Unsafe race:pwent functions (i.e. -// setpwent/getpwent/endpwent). -var pwentLock sync.Mutex - -// getHostUsers returns the list of all users on the host from the user -// directory (depending on system configuration this can be /etc/passwd, -// LDAP...). -func getHostUsers() ([]user.User, error) { - pwentLock.Lock() - defer pwentLock.Unlock() - - C.setpwent() - defer C.endpwent() - - var results []user.User - for { - result, err := C.getpwent() - // cgo error convention, check the return value before errno - if result != nil { - results = append(results, passwdC2Go(result)) - continue - } - if err != nil { - return nil, trace.Wrap(err) - } - return results, nil - } -} - -// passwdC2Go converts `passwd` struct from C to golang native struct -func passwdC2Go(passwdC *C.struct_passwd) user.User { - name := C.GoString(passwdC.pw_name) - return user.User{ - Name: name, - Username: name, - Uid: strconv.FormatUint(uint64(passwdC.pw_uid), 10), - Gid: strconv.FormatUint(uint64(passwdC.pw_gid), 10), - HomeDir: C.GoString(passwdC.pw_dir), - } -} diff --git a/lib/srv/desktop/linux_server.go b/lib/srv/desktop/linux_server.go index 76a038e8944..ca334f0e311 100644 --- a/lib/srv/desktop/linux_server.go +++ b/lib/srv/desktop/linux_server.go @@ -26,7 +26,6 @@ import ( "maps" "net" "os" - "os/user" "regexp" "slices" "strconv" @@ -65,6 +64,7 @@ import ( "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/reexec" ) @@ -749,11 +749,11 @@ func (sess *linuxSession) handleClientHello(m *tdpb.ClientHello) error { } func (sess *linuxSession) changeAuthorityFileOwnership(m *tdpb.ClientHello) error { - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return trace.Wrap(err, "failed to get current user") } - targetUser, err := user.Lookup(m.Username) + targetUser, err := hostuser.Lookup(m.Username) if err != nil { return trace.Wrap(err, "failed to lookup user %s", m.Username) } diff --git a/lib/srv/desktop/x11/xsession.go b/lib/srv/desktop/x11/xsession.go index b0ab7ce6e20..6abc347fba8 100644 --- a/lib/srv/desktop/x11/xsession.go +++ b/lib/srv/desktop/x11/xsession.go @@ -23,7 +23,6 @@ import ( "log/slog" "os" "os/exec" - "os/user" "path/filepath" "regexp" "strings" @@ -36,6 +35,7 @@ import ( reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec" logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/session/envutils" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/reexec" "github.com/gravitational/teleport/session/reexec/reexecconstants" ) @@ -126,7 +126,7 @@ func StartTeleportExecXSession(ctx context.Context, cfg *XSessionConfig) (*reexe return nil, trace.BadParameter("missing parameter ChildLogConfig") } - if _, err := user.Lookup(cfg.Login); err != nil { + if _, err := hostuser.Lookup(cfg.Login); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/srv/mcp/stdio.go b/lib/srv/mcp/stdio.go index 264391aa9ba..8b97ed4bdf7 100644 --- a/lib/srv/mcp/stdio.go +++ b/lib/srv/mcp/stdio.go @@ -26,7 +26,6 @@ import ( "net" "os" "os/exec" - "os/user" "sync" "syscall" "time" @@ -36,6 +35,7 @@ import ( "github.com/gravitational/teleport/lib/utils/mcputils" hostutils "github.com/gravitational/teleport/session/host" + hostuser "github.com/gravitational/teleport/session/host/user" ) // handleAuthErrStdio starts a stdio message reader and replies with the auth @@ -238,7 +238,7 @@ func makeExecServerRunner(ctx context.Context, session *sessionHandler) (stdioSe } // Set host user. - hostUser, err := user.Lookup(mcpSpec.RunAsHostUser) + hostUser, err := hostuser.Lookup(mcpSpec.RunAsHostUser) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/srv/regular/sshserver.go b/lib/srv/regular/sshserver.go index b2f1ed9924a..06c57ea9237 100644 --- a/lib/srv/regular/sshserver.go +++ b/lib/srv/regular/sshserver.go @@ -75,6 +75,7 @@ import ( "github.com/gravitational/teleport/lib/sshutils" reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec" "github.com/gravitational/teleport/lib/utils" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/networking" "github.com/gravitational/teleport/session/networking/x11" "github.com/gravitational/teleport/session/pam/pamcfg" @@ -1103,7 +1104,7 @@ func (s *Server) startAuthorizedKeysManager(ctx context.Context, auth authclient Clock: s.clock, }, ) - if errors.Is(err, authorizedkeysreporter.ErrUnsupportedPlatform) { + if errors.Is(err, hostuser.ErrUnsupportedPlatform) { return nil } else if err != nil { return trace.Wrap(err) diff --git a/lib/srv/term.go b/lib/srv/term.go index 4da82245e89..42edd3202d1 100644 --- a/lib/srv/term.go +++ b/lib/srv/term.go @@ -41,6 +41,7 @@ import ( tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" rsession "github.com/gravitational/teleport/lib/session" reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/reexec" "github.com/gravitational/teleport/session/reexec/reexecconstants" ) @@ -535,7 +536,7 @@ func getOwner(login string, lookupUser LookupUser, lookupGroup LookupGroup) (int // setOwner changes the owner and mode of the TTY. func (t *terminal) setOwner() error { - uid, gid, mode, err := getOwner(t.serverContext.Identity.Login, user.Lookup, user.LookupGroup) + uid, gid, mode, err := getOwner(t.serverContext.Identity.Login, hostuser.Lookup, hostuser.LookupGroup) if err != nil { return trace.Wrap(err) } diff --git a/lib/srv/usermgmt_linux.go b/lib/srv/usermgmt_linux.go index fcf0b589ceb..9a92c266d38 100644 --- a/lib/srv/usermgmt_linux.go +++ b/lib/srv/usermgmt_linux.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/session/host" + hostuser "github.com/gravitational/teleport/session/host/user" ) // HostUsersProvisioningBackend is used to implement HostUsersBackend @@ -74,7 +75,7 @@ func newHostSudoersBackend(uuid string) (HostSudoersBackend, error) { // Lookup implements host user information lookup func (*HostUsersProvisioningBackend) Lookup(username string) (*user.User, error) { - usr, err := user.Lookup(username) + usr, err := hostuser.Lookup(username) if err != nil { if !errors.Is(err, user.UnknownUserError(username)) && strings.Contains(err.Error(), "no such file or directory") { return nil, trace.Wrap(err, "looking up user %q, sources configured for passwd in host's /etc/nsswitch.conf may be misconfigured", username) @@ -88,17 +89,17 @@ func (*HostUsersProvisioningBackend) Lookup(username string) (*user.User, error) // UserGIDs returns the list of group IDs for a user func (*HostUsersProvisioningBackend) UserGIDs(u *user.User) ([]string, error) { - return u.GroupIds() + return hostuser.GroupIds(u) } // LookupGroup host group information lookup func (*HostUsersProvisioningBackend) LookupGroup(name string) (*user.Group, error) { - return user.LookupGroup(name) + return hostuser.LookupGroup(name) } // LookupGroup host group information lookup by GID func (*HostUsersProvisioningBackend) LookupGroupByID(gid string) (*user.Group, error) { - return user.LookupGroupId(gid) + return hostuser.LookupGroupId(gid) } // UpdateUser sets a user's groups and default shell, replacing their existing groups. diff --git a/lib/sshutils/reexec/reexec.go b/lib/sshutils/reexec/reexec.go index 061ac354d22..079a0c18bdd 100644 --- a/lib/sshutils/reexec/reexec.go +++ b/lib/sshutils/reexec/reexec.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/trace" decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1" + hostuser "github.com/gravitational/teleport/session/host/user" ) // ErrorContext contains context used to enrich child process launch errors. @@ -65,7 +66,7 @@ func ChildErrorWithContext(errMsg string, context *ErrorContext) string { unknownUserError := user.UnknownUserError(context.Login) switch { case strings.Contains(errMsg, "failed to open PAM context"): // PAM errors are often cause by an unknown user. - if _, err := user.Lookup(context.Login); errors.Is(err, unknownUserError) { + if _, err := hostuser.Lookup(context.Login); errors.Is(err, unknownUserError) { if ambiguousHostUserDenial { return ambiguousHostUserError() } diff --git a/lib/tbot/bot/destination/directory.go b/lib/tbot/bot/destination/directory.go index 3e32ed8651a..eb76161b364 100644 --- a/lib/tbot/bot/destination/directory.go +++ b/lib/tbot/bot/destination/directory.go @@ -24,7 +24,6 @@ import ( "fmt" "io/fs" "os" - "os/user" "path" "path/filepath" "strings" @@ -37,6 +36,7 @@ import ( "github.com/gravitational/teleport/lib/tbot/botfs" "github.com/gravitational/teleport/lib/tbot/internal/encoding" "github.com/gravitational/teleport/lib/utils" + hostuser "github.com/gravitational/teleport/session/host/user" ) const DirectoryType = "directory" @@ -233,7 +233,7 @@ func (dd *Directory) ACLsEnabled() bool { // This will not attempt to correct any issues, but will cause a hard failure if // `acls: required` is configured and issues are detected. func (dd *Directory) verifyLegacyACLs(keys []string) error { - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { // user.Current will fail if the user id does not exist in /etc/passwd // as is the case with some containerized environments. diff --git a/lib/tbot/botfs/fs_linux.go b/lib/tbot/botfs/fs_linux.go index ce5947d0544..b6097c26586 100644 --- a/lib/tbot/botfs/fs_linux.go +++ b/lib/tbot/botfs/fs_linux.go @@ -39,6 +39,7 @@ import ( "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/set" + hostuser "github.com/gravitational/teleport/session/host/user" ) // mostACLRead is a permission mode granting readonly access to a file. @@ -485,7 +486,7 @@ func resolveACLReaderSelector(s *ACLSelector, dir bool) (acl.Entry, error) { }, nil } - user, err := user.Lookup(s.User) + user, err := hostuser.Lookup(s.User) if err != nil { return acl.Entry{}, trace.Wrap(err) } @@ -506,7 +507,7 @@ func resolveACLReaderSelector(s *ACLSelector, dir bool) (acl.Entry, error) { }, nil } - group, err := user.LookupGroup(s.Group) + group, err := hostuser.LookupGroup(s.Group) if err != nil { return acl.Entry{}, trace.Wrap(err) } @@ -711,7 +712,7 @@ func GetOwner(fileInfo fs.FileInfo) (*user.User, error) { return nil, trace.NotImplemented("Cannot verify file ownership on this platform.") } - user, err := user.LookupId(strconv.Itoa(int(info.Uid))) + user, err := hostuser.LookupId(strconv.Itoa(int(info.Uid))) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/services/connectmycomputer/connectmycomputer.go b/lib/teleterm/services/connectmycomputer/connectmycomputer.go index aa40d5181cc..ec4ad2be9f1 100644 --- a/lib/teleterm/services/connectmycomputer/connectmycomputer.go +++ b/lib/teleterm/services/connectmycomputer/connectmycomputer.go @@ -23,7 +23,6 @@ import ( "fmt" "log/slog" "os" - "os/user" "path/filepath" "slices" "strings" @@ -42,6 +41,7 @@ import ( "github.com/gravitational/teleport/lib/teleterm/clusters" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/hostid" + hostuser "github.com/gravitational/teleport/session/host/user" ) type RoleSetup struct { @@ -98,7 +98,7 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity } } - systemUser, err := user.Current() + systemUser, err := hostuser.Current() if err != nil { return noCertsReloaded, trace.Wrap(err) } diff --git a/lib/utils/disk.go b/lib/utils/disk.go index 8078d98a017..608e6fd140e 100644 --- a/lib/utils/disk.go +++ b/lib/utils/disk.go @@ -30,6 +30,8 @@ import ( "syscall" "github.com/gravitational/trace" + + hostuser "github.com/gravitational/teleport/session/host/user" ) // PercentUsed returns percentage of disk space used. The percentage of disk @@ -93,12 +95,12 @@ func CanUserWriteTo(path string) (bool, error) { var usr *user.User if ogUser := os.Getenv("SUDO_USER"); ogUser != "" { - usr, err = user.Lookup(ogUser) + usr, err = hostuser.Lookup(ogUser) if err != nil { return false, trace.NotFound("could not determine original user: %+v", err) } } else { - usr, err = user.Current() + usr, err = hostuser.Current() if err != nil { return false, trace.NotFound("could not determine current user: %+v", err) } @@ -116,7 +118,7 @@ func CanUserWriteTo(path string) (bool, error) { } // file and user have a group in common - groupIDs, err := usr.GroupIds() + groupIDs, err := hostuser.GroupIds(usr) if err != nil { return false, trace.NotFound("could not determine current user group ids: %+v", err) } diff --git a/session/host/hostusers.go b/session/host/hostusers.go index 144786ddbe6..d1b2027a729 100644 --- a/session/host/hostusers.go +++ b/session/host/hostusers.go @@ -34,6 +34,8 @@ import ( "syscall" "github.com/gravitational/trace" + + hostuser "github.com/gravitational/teleport/session/host/user" ) // man GROUPADD(8), exit codes section @@ -181,7 +183,7 @@ func UserDel(username string) (exitCode int, err error) { if err != nil { return -1, trace.NotFound("cant find userdel binary: %s", err) } - u, err := user.Lookup(username) + u, err := hostuser.Lookup(username) if err != nil { return -1, trace.Wrap(err) } @@ -390,7 +392,7 @@ func GetHostUserCredential(localUser *user.User) (*syscall.Credential, error) { } // Lookup supplementary groups for the user. - userGroups, err := localUser.GroupIds() + userGroups, err := hostuser.GroupIds(localUser) if err != nil { return nil, trace.Wrap(err) } diff --git a/session/host/user/common.go b/session/host/user/common.go new file mode 100644 index 00000000000..46fa9000e39 --- /dev/null +++ b/session/host/user/common.go @@ -0,0 +1,22 @@ +// 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 . + +package user + +import "errors" + +// ErrUnsupportedPlatform is returned when the operating system is not supported. +var ErrUnsupportedPlatform = errors.New("unsupported platform") diff --git a/session/host/user/doc.go b/session/host/user/doc.go new file mode 100644 index 00000000000..0f64e8bb36e --- /dev/null +++ b/session/host/user/doc.go @@ -0,0 +1,42 @@ +// 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 . + +// Package user provides a thin concurrency safe wrapper around the +// `os/user` APIs for Linux when CGO is enabled. +// +// On Linux, the `os/user` package may call into C-based NSS backends via cgo. +// Some NSS implementations (or configurations) are not safe for concurrent +// use and can deadlock or crash when lookups are performed concurrently. +// +// To avoid such failures, this package serializes calls to a small set of `os/user` +// APIs on linux+cgo builds. +// Non-Linux or non-cgo builds forward calls to the stdlib without locking. +// +// References: +// +// - Debian bug report: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=831390 +// - Teleport issue: https://github.com/gravitational/teleport/issues/69662 +// +// Note that this package is placed in `session/host` specifically because the `session` tree +// is effectively a submodule within Teleport which guards against any imports from Teleport itself. +// This was done to optimize the session helper load times and avoid bleeding runtime dependencies into the +// session helper which would incease the load time and degrade latency. Since Teleport still calls +// into the session helper code in the same process for which a mutex must be shared, the user package +// is placed here despite less than ideal code organization. +package user + +// TODO(okraport): revisit this module placement and investigate moving it to lib/utils/user if possible. +// TODO(okraport): revisit this module to consider replacement with getent process shell out to avoid the mutex. diff --git a/lib/secretsscanner/authorizedkeys/users_list_darwin.go b/session/host/user/user_darwin.go similarity index 93% rename from lib/secretsscanner/authorizedkeys/users_list_darwin.go rename to session/host/user/user_darwin.go index 7eeb334bfbf..d13a0663d90 100644 --- a/lib/secretsscanner/authorizedkeys/users_list_darwin.go +++ b/session/host/user/user_darwin.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package authorizedkeys +package user /* #include @@ -31,10 +31,10 @@ import ( "github.com/gravitational/trace" ) -// getHostUsers returns the list of all users on the host from the user +// GetHostUsers returns the list of all users on the host from the user // directory (depending on system configuration this can be /etc/passwd, // LDAP...). -func getHostUsers() ([]user.User, error) { +func GetHostUsers() ([]user.User, error) { // on darwin the setpwent/getpwent/endpwent functions use thread-local // storage so there's no need for a global lock but we must call the whole // sequence from the same thread diff --git a/session/host/user/user_forward.go b/session/host/user/user_forward.go new file mode 100644 index 00000000000..86b3495027b --- /dev/null +++ b/session/host/user/user_forward.go @@ -0,0 +1,48 @@ +//go:build !linux || !cgo + +// 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 . + +package user + +import ( + osu "os/user" + + "github.com/gravitational/trace" +) + +// Lookup wraps [os/user.Lookup]. +func Lookup(username string) (*osu.User, error) { return osu.Lookup(username) } + +// LookupId wraps [os/user.LookupId]. +func LookupId(id string) (*osu.User, error) { return osu.LookupId(id) } + +// LookupGroup wraps [os/user.LookupGroup]. +func LookupGroup(name string) (*osu.Group, error) { return osu.LookupGroup(name) } + +// LookupGroupId wraps [os/user.LookupGroupId]. +func LookupGroupId(id string) (*osu.Group, error) { return osu.LookupGroupId(id) } + +// Current wraps [os/user.Current]. +func Current() (*osu.User, error) { return osu.Current() } + +// GroupIds wraps [os/user.User.GroupIds]. +func GroupIds(u *osu.User) ([]string, error) { + if u == nil { + return nil, trace.BadParameter("user cannot be nil") + } + return u.GroupIds() +} diff --git a/session/host/user/user_linux_cgo.go b/session/host/user/user_linux_cgo.go new file mode 100644 index 00000000000..e16a4aabb45 --- /dev/null +++ b/session/host/user/user_linux_cgo.go @@ -0,0 +1,120 @@ +//go:build linux && cgo + +// 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 . + +package user + +/* +#define _XOPEN_SOURCE 500 +#include +*/ +import "C" + +import ( + osuser "os/user" + "strconv" + "sync" + + "github.com/gravitational/trace" +) + +var packageMU sync.Mutex + +// Lookup wraps [os/user.Lookup] with process-wide serialization. +func Lookup(username string) (*osuser.User, error) { + packageMU.Lock() + defer packageMU.Unlock() + return osuser.Lookup(username) +} + +// LookupId wraps [os/user.LookupId] with process-wide serialization. +func LookupId(id string) (*osuser.User, error) { + packageMU.Lock() + defer packageMU.Unlock() + return osuser.LookupId(id) +} + +// LookupGroup wraps [os/user.LookupGroup] with process-wide serialization. +func LookupGroup(name string) (*osuser.Group, error) { + packageMU.Lock() + defer packageMU.Unlock() + return osuser.LookupGroup(name) +} + +// LookupGroupId wraps [os/user.LookupGroupId] with process-wide serialization. +func LookupGroupId(id string) (*osuser.Group, error) { + packageMU.Lock() + defer packageMU.Unlock() + return osuser.LookupGroupId(id) +} + +// Current wraps [os/user.Current] with process-wide serialization. +func Current() (*osuser.User, error) { + packageMU.Lock() + defer packageMU.Unlock() + return osuser.Current() +} + +// LookGroupIdsup wraps [os/user.User.GroupIds] with process-wide serialization. +func GroupIds(u *osuser.User) ([]string, error) { + if u == nil { + return nil, trace.BadParameter("user cannot be nil") + } + packageMU.Lock() + defer packageMU.Unlock() + return u.GroupIds() +} + +// GetHostUsers returns the list of all users on the host from the user +// directory (depending on system configuration this can be /etc/passwd, +// LDAP...). +func GetHostUsers() ([]osuser.User, error) { + // A lock should be acquired when using MT-Unsafe race:pwent functions (i.e. + // setpwent/getpwent/endpwent). Since `getpwent` can follow the same path + // into libnss we take the package lock. + packageMU.Lock() + defer packageMU.Unlock() + + C.setpwent() + defer C.endpwent() + + var results []osuser.User + for { + result, err := C.getpwent() + // cgo error convention, check the return value before errno + if result != nil { + results = append(results, passwdC2Go(result)) + continue + } + if err != nil { + return nil, trace.Wrap(err) + } + return results, nil + } +} + +// passwdC2Go converts `passwd` struct from C to golang native struct +func passwdC2Go(passwdC *C.struct_passwd) osuser.User { + name := C.GoString(passwdC.pw_name) + return osuser.User{ + Name: name, + Username: name, + Uid: strconv.FormatUint(uint64(passwdC.pw_uid), 10), + Gid: strconv.FormatUint(uint64(passwdC.pw_gid), 10), + HomeDir: C.GoString(passwdC.pw_dir), + } +} diff --git a/lib/secretsscanner/authorizedkeys/users_list_other.go b/session/host/user/user_other.go similarity index 94% rename from lib/secretsscanner/authorizedkeys/users_list_other.go rename to session/host/user/user_other.go index 5355306b850..f086722e8a6 100644 --- a/lib/secretsscanner/authorizedkeys/users_list_other.go +++ b/session/host/user/user_other.go @@ -16,7 +16,7 @@ //go:build !darwin && !linux -package authorizedkeys +package user import ( "os/user" @@ -29,7 +29,7 @@ var alwaysFalse bool // getHostUsers returns ErrUnsupportedPlatform because this platform is not // supported. On supported platforms, it returns the list of all users on the // host from the user directory. -func getHostUsers() ([]user.User, error) { +func GetHostUsers() ([]user.User, error) { if alwaysFalse { // thwart the well-meaning intentions of staticcheck return nil, nil diff --git a/session/reexec/reexec.go b/session/reexec/reexec.go index 8df968a4495..d8a8d8b2ac7 100644 --- a/session/reexec/reexec.go +++ b/session/reexec/reexec.go @@ -48,6 +48,7 @@ import ( "github.com/gravitational/teleport/session/auditd" "github.com/gravitational/teleport/session/envutils" "github.com/gravitational/teleport/session/host" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/logconstants" "github.com/gravitational/teleport/session/loginuid" "github.com/gravitational/teleport/session/networking" @@ -468,7 +469,7 @@ func RunCommand() (exitErr error, err error) { WtmpdbFile: c.UaccMetadata.WtmpdbPath, }) - localUser, err := user.Lookup(c.Login) + localUser, err := hostuser.Lookup(c.Login) if err != nil { if uaccErr := uaccHandler.FailedLogin(c.Login, &c.UaccMetadata.RemoteAddr); uaccErr != nil { slog.DebugContext(ctx, "unable to write failed login attempt to uacc", "error", uaccErr) @@ -671,8 +672,8 @@ type osWrapper struct { func newOsWrapper() *osWrapper { return &osWrapper{ - LookupGroup: user.LookupGroup, - LookupUser: user.Lookup, + LookupGroup: hostuser.LookupGroup, + LookupUser: hostuser.Lookup, CommandContext: exec.CommandContext, } } @@ -698,7 +699,7 @@ func (s *systemUser) UID() string { } func (s *systemUser) GroupIds() ([]string, error) { - return s.u.GroupIds() + return hostuser.GroupIds(s.u) } // startNewParker starts a new parker process only if the requested user has been created @@ -810,7 +811,7 @@ func RunNetworking() (code int, err error) { // Once the PAM stack is called with parent process permissions, set the process uid // and gid to the requested user. This way, the user's networking requests will be // done with the user's permissions. - localUser, err := user.Lookup(c.Login) + localUser, err := hostuser.Lookup(c.Login) if err != nil { return reexecconstants.RemoteCommandFailure, trace.NotFound("%s", err) } @@ -1231,7 +1232,7 @@ func openFileAsUser(localUser *user.User, path string) (file *os.File, err error return nil, trace.Wrap(err) } - strIDs, err := localUser.GroupIds() + strIDs, err := hostuser.GroupIds(localUser) if err != nil { return nil, trace.Wrap(err) } @@ -1480,7 +1481,7 @@ var accessibleHomeDirMu sync.Mutex // hasAccessibleHomeDir checks if the current user has access to an existing home directory. func hasAccessibleHomeDir() error { // this should usually be fetching a cached value - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return trace.Wrap(err) } @@ -1522,7 +1523,7 @@ func hasAccessibleHomeDir() error { // 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) { - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return false, trace.Wrap(err) } diff --git a/session/reexec/reexecsftp/sftp.go b/session/reexec/reexecsftp/sftp.go index 3edf39d51c3..8012b76425e 100644 --- a/session/reexec/reexecsftp/sftp.go +++ b/session/reexec/reexecsftp/sftp.go @@ -26,7 +26,6 @@ import ( "io" "log/slog" "os" - "os/user" "path" "path/filepath" "strings" @@ -38,6 +37,7 @@ import ( "github.com/pkg/sftp" "golang.org/x/sys/unix" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/reexec/safefile" "github.com/gravitational/teleport/session/sftputils" ) @@ -305,7 +305,7 @@ func RunSFTP(logger *slog.Logger) error { } defer auditFile.Close() - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return trace.Wrap(err) } diff --git a/session/shell/shell_unix.go b/session/shell/shell_unix.go index 2de21be2724..f3a75d2ef62 100644 --- a/session/shell/shell_unix.go +++ b/session/shell/shell_unix.go @@ -37,18 +37,19 @@ import "C" import ( "context" "log/slog" - "os/user" "strings" "syscall" "unsafe" "github.com/gravitational/trace" + + hostuser "github.com/gravitational/teleport/session/host/user" ) // getLoginShell determines the login shell for a given username func getLoginShell(username string) (string, error) { // See if the username is valid. - _, err := user.Lookup(username) + _, err := hostuser.Lookup(username) if err != nil { return "", trace.Wrap(err) } diff --git a/tool/tbot/init.go b/tool/tbot/init.go index ecbbc833e8e..42fa7fc6e88 100644 --- a/tool/tbot/init.go +++ b/tool/tbot/init.go @@ -37,6 +37,7 @@ import ( "github.com/gravitational/teleport/lib/tbot/cli" "github.com/gravitational/teleport/lib/tbot/config" "github.com/gravitational/teleport/lib/tbot/identity" + hostuser "github.com/gravitational/teleport/session/host/user" ) // RootUID is the UID of the root user @@ -207,7 +208,7 @@ func ensurePermissions( return trace.BadParameter("File %s is expected to be a file but is a directory", path) } - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return trace.Wrap(err) } @@ -319,12 +320,12 @@ func parseOwnerString(owner string) (*user.User, *user.Group, error) { return nil, nil, trace.BadParameter("invalid owner string: %q", owner) } - ownerUser, err := user.Lookup(ownerParts[0]) + ownerUser, err := hostuser.Lookup(ownerParts[0]) if err != nil { return nil, nil, trace.Wrap(err) } - ownerGroup, err := user.LookupGroup(ownerParts[1]) + ownerGroup, err := hostuser.LookupGroup(ownerParts[1]) if err != nil { return nil, nil, trace.Wrap(err) } @@ -358,12 +359,12 @@ func getOwner(cliOwner, defaultOwner string) (*user.User, *user.Group, error) { log.DebugContext(context.TODO(), "Will use current user as owner") // Otherwise, return the current user and group - currentUser, err := user.Current() + currentUser, err := hostuser.Current() if err != nil { return nil, nil, trace.Wrap(err) } - currentGroup, err := user.LookupGroupId(currentUser.Gid) + currentGroup, err := hostuser.LookupGroupId(currentUser.Gid) if err != nil { return nil, nil, trace.Wrap(err) } @@ -383,17 +384,17 @@ func getAndTestACLOptions(initCmd *cli.InitCommand, destDir string) (*user.User, return nil, nil, nil, trace.BadParameter("--reader-user must be set") } - botUser, err := user.Lookup(initCmd.BotUser) + botUser, err := hostuser.Lookup(initCmd.BotUser) if err != nil { return nil, nil, nil, trace.Wrap(err) } - botGroup, err := user.LookupGroupId(botUser.Gid) + botGroup, err := hostuser.LookupGroupId(botUser.Gid) if err != nil { return nil, nil, nil, trace.Wrap(err) } - readerUser, err := user.Lookup(initCmd.ReaderUser) + readerUser, err := hostuser.Lookup(initCmd.ReaderUser) if err != nil { return nil, nil, nil, trace.Wrap(err) } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index b972c1775dd..eec29d663aa 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -27,7 +27,6 @@ import ( "maps" "net/url" "os" - "os/user" "path" "path/filepath" "runtime" @@ -61,6 +60,7 @@ import ( "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/versioncontrol" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/reexec" "github.com/gravitational/teleport/session/reexec/reexecconstants" "github.com/gravitational/teleport/session/selinux" @@ -1230,7 +1230,7 @@ func onSCP(scpFlags *scp.Flags) error { } // get user's home dir (it serves as a default destination) - user, err := user.Current() + user, err := hostuser.Current() if err != nil { return trace.Wrap(err) } diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index cae51bd7d7f..20bdc815db8 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -33,7 +33,6 @@ import ( "os" "os/exec" "os/signal" - "os/user" "path/filepath" "regexp" "runtime" @@ -106,6 +105,7 @@ import ( "github.com/gravitational/teleport/lib/utils/mlock" "github.com/gravitational/teleport/lib/utils/parse" stacksignal "github.com/gravitational/teleport/lib/utils/signal" + hostuser "github.com/gravitational/teleport/session/host/user" "github.com/gravitational/teleport/session/networking/x11" "github.com/gravitational/teleport/session/shell" "github.com/gravitational/teleport/tool/common" @@ -4453,7 +4453,7 @@ func onSSHLatency(cf *CLIConf) error { func runLocalCommand(hostLogin string, command []string) error { if len(command) == 0 { if hostLogin == "" { - user, err := user.Current() + user, err := hostuser.Current() if err != nil { return trace.Wrap(err) }