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)
}