Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 38 additions & 35 deletions integration/utmp_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package integration

import (
"context"
"database/sql"
"os"
"os/user"
"path/filepath"
Expand Down Expand Up @@ -72,6 +73,13 @@ type SrvCtx struct {
utmpPath string
wtmpPath string
btmpPath string
wtmpdbPath string
}

func checkUserInFile(t assert.TestingT, utmp *uacc.UtmpBackend, uaccFile, username string, expectPresent bool) {
inFile, err := utmp.IsUserInFile(uaccFile, username)
assert.NoError(t, err)
assert.Equal(t, expectPresent, inFile)
}

// TestRootUTMPEntryExists verifies that user accounting is done on supported systems.
Expand All @@ -89,6 +97,11 @@ func TestRootUTMPEntryExists(t *testing.T) {
up, err := newUpack(ctx, s, teleportTestUser, []string{teleportTestUser, teleportFakeUser}, wildcardAllow)
require.NoError(t, err)

utmp, err := uacc.NewUtmpBackend(s.utmpPath, s.wtmpPath, s.btmpPath)
require.NoError(t, err)
wtmpdb, err := uacc.NewWtmpdbBackend(s.wtmpdbPath)
require.NoError(t, err)

t.Run("successful login is logged in utmp and wtmp", func(t *testing.T) {
sshConfig := &ssh.ClientConfig{
User: teleportTestUser,
Expand Down Expand Up @@ -118,10 +131,14 @@ func TestRootUTMPEntryExists(t *testing.T) {
require.NoError(t, err)

require.EventuallyWithTf(t, func(collect *assert.CollectT) {
assert.NoError(collect, uacc.UserWithPtyInDatabase(s.utmpPath, teleportTestUser))
assert.NoError(collect, uacc.UserWithPtyInDatabase(s.wtmpPath, teleportTestUser))
checkUserInFile(collect, utmp, s.utmpPath, teleportTestUser, true)
checkUserInFile(collect, utmp, s.wtmpPath, teleportTestUser, true)
// Ensure than an entry was not written to btmp.
assert.True(collect, trace.IsNotFound(uacc.UserWithPtyInDatabase(s.btmpPath, teleportTestUser)), "unexpected error: %v", err)
checkUserInFile(collect, utmp, s.btmpPath, teleportTestUser, false)

inWtmpdb, err := wtmpdb.IsUserLoggedIn(teleportTestUser)
assert.NoError(collect, err)
assert.True(collect, inWtmpdb)
}, 5*time.Minute, time.Second, "did not detect utmp entry within 5 minutes")
})

Expand Down Expand Up @@ -154,43 +171,19 @@ func TestRootUTMPEntryExists(t *testing.T) {
require.NoError(t, err)

require.EventuallyWithT(t, func(collect *assert.CollectT) {
assert.NoError(collect, uacc.UserWithPtyInDatabase(s.btmpPath, teleportFakeUser))
checkUserInFile(collect, utmp, s.btmpPath, teleportFakeUser, true)
// Ensure that entries were not written to utmp and wtmp
assert.True(collect, trace.IsNotFound(uacc.UserWithPtyInDatabase(s.utmpPath, teleportFakeUser)), "unexpected error: %v", err)
assert.True(collect, trace.IsNotFound(uacc.UserWithPtyInDatabase(s.wtmpPath, teleportFakeUser)), "unexpected error: %v", err)
checkUserInFile(collect, utmp, s.utmpPath, teleportFakeUser, false)
checkUserInFile(collect, utmp, s.wtmpPath, teleportFakeUser, false)

inWtmpdb, err := wtmpdb.IsUserLoggedIn(teleportFakeUser)
assert.NoError(collect, err)
assert.False(collect, inWtmpdb)
}, 5*time.Minute, time.Second, "did not detect btmp entry within 5 minutes")
})

}

// TestUsernameLimit tests that the maximum length of usernames is a hard error.
func TestRootUsernameLimit(t *testing.T) {
if !isRoot() {
t.Skip("This test will be skipped because tests are not being run as root.")
}

dir := t.TempDir()
utmpPath := filepath.Join(dir, "utmp")
wtmpPath := filepath.Join(dir, "wtmp")

err := TouchFile(utmpPath)
require.NoError(t, err)
err = TouchFile(wtmpPath)
require.NoError(t, err)

// A 33 character long username.
username := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
host := [4]int32{0, 0, 0, 0}
tty := os.NewFile(uintptr(0), "/proc/self/fd/0")
err = uacc.Open(utmpPath, wtmpPath, username, "localhost", host, tty)
require.True(t, trace.IsBadParameter(err))

// A 32 character long username.
username = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
err = uacc.Open(utmpPath, wtmpPath, username, "localhost", host, tty)
require.False(t, trace.IsBadParameter(err))
}

// upack holds all ssh signing artifacts needed for signing and checking user keys
type upack struct {
// key is a raw private user key
Expand Down Expand Up @@ -290,12 +283,22 @@ func newSrvCtx(ctx context.Context, t *testing.T) *SrvCtx {
utmpPath := filepath.Join(uaccDir, "utmp")
wtmpPath := filepath.Join(uaccDir, "wtmp")
btmpPath := filepath.Join(uaccDir, "btmp")
wtmpdbPath := filepath.Join(uaccDir, "wtmp.db")
require.NoError(t, TouchFile(utmpPath))
require.NoError(t, TouchFile(wtmpPath))
require.NoError(t, TouchFile(btmpPath))
require.NoError(t, TouchFile(wtmpdbPath))
s.utmpPath = utmpPath
s.wtmpPath = wtmpPath
s.btmpPath = btmpPath
s.wtmpdbPath = wtmpdbPath

// Initialize wtmpdb database.
db, err := sql.Open("sqlite3", wtmpdbPath)
require.NoError(t, err)
// Schema: https://github.com/thkukuk/wtmpdb/blob/272b109f5b3bdfb3008604461b4ddbff03c28b77/lib/sqlite.c#L128
_, err = db.Exec("CREATE TABLE IF NOT EXISTS wtmp(ID INTEGER PRIMARY KEY, Type INTEGER, User TEXT NOT NULL, Login INTEGER, Logout INTEGER, TTY TEXT, RemoteHost TEXT, Service TEXT) STRICT;")
require.NoError(t, err)

lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Expand Down Expand Up @@ -342,7 +345,7 @@ func newSrvCtx(ctx context.Context, t *testing.T) *SrvCtx {
),
regular.SetBPF(&bpf.NOP{}),
regular.SetClock(s.clock),
regular.SetUserAccountingPaths(utmpPath, wtmpPath, btmpPath),
regular.SetUserAccountingPaths(utmpPath, wtmpPath, btmpPath, wtmpdbPath),
regular.SetLockWatcher(lockWatcher),
regular.SetSessionController(nodeSessionController),
)
Expand Down
19 changes: 4 additions & 15 deletions lib/srv/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import (
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
rsession "github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv/uacc"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/x11"
Expand Down Expand Up @@ -168,7 +167,7 @@ type Server interface {
Context() context.Context

// GetUserAccountingPaths returns the path of the user accounting database and log. Returns empty for system defaults.
GetUserAccountingPaths() (utmp, wtmp, btmp string)
GetUserAccountingPaths() (utmp, wtmp, btmp, wtmpdb string)

// GetLockWatcher gets the server's lock watcher.
GetLockWatcher() *services.LockWatcher
Expand Down Expand Up @@ -1175,23 +1174,13 @@ func closeAll(closers ...io.Closer) error {
}

func newUaccMetadata(c *ServerContext) (*UaccMetadata, error) {
addr := c.ConnectionContext.ServerConn.RemoteAddr()
hostname, _, err := net.SplitHostPort(addr.String())
if err != nil {
return nil, trace.Wrap(err)
}
preparedAddr, err := uacc.PrepareAddr(addr)
if err != nil {
return nil, trace.Wrap(err)
}
utmpPath, wtmpPath, btmpPath := c.srv.GetUserAccountingPaths()

utmpPath, wtmpPath, btmpPath, wtmpdbPath := c.srv.GetUserAccountingPaths()
return &UaccMetadata{
Hostname: hostname,
RemoteAddr: preparedAddr,
RemoteAddr: utils.FromAddr(c.ConnectionContext.ServerConn.RemoteAddr()),
UtmpPath: utmpPath,
WtmpPath: wtmpPath,
BtmpPath: btmpPath,
WtmpdbPath: wtmpdbPath,
}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions lib/srv/forward/sshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,8 @@ func (s *Server) GetClock() clockwork.Clock {
// GetUserAccountingPaths returns the optional override of the utmp, wtmp, and btmp path.
// These values are never set for the forwarding server because utmp, wtmp, and btmp
// are updated by the target server and not the forwarding server.
func (s *Server) GetUserAccountingPaths() (string, string, string) {
return "", "", ""
func (s *Server) GetUserAccountingPaths() (string, string, string, string) {
return "", "", "", ""
}

// GetLockWatcher gets the server's lock watcher.
Expand Down
2 changes: 1 addition & 1 deletion lib/srv/git/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ func (s *ForwardServer) UseTunnel() bool {
func (s *ForwardServer) GetBPF() bpf.BPF {
return nil
}
func (s *ForwardServer) GetUserAccountingPaths() (utmp, wtmp, btmp string) {
func (s *ForwardServer) GetUserAccountingPaths() (utmp, wtmp, btmp, wtmpdb string) {
return
}
func (s *ForwardServer) GetLockWatcher() *services.LockWatcher {
Expand Down
4 changes: 2 additions & 2 deletions lib/srv/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ func (m *mockServer) Context() context.Context {
}

// GetUserAccountingPaths returns the path of the user accounting database and log. Returns empty for system defaults.
func (m *mockServer) GetUserAccountingPaths() (utmp, wtmp, btmp string) {
return "test", "test", "test"
func (m *mockServer) GetUserAccountingPaths() (utmp, wtmp, btmp, wtmpdb string) {
return "test", "test", "test", "test"
}

// GetLockWatcher gets the server's lock watcher.
Expand Down
42 changes: 22 additions & 20 deletions lib/srv/reexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,8 @@ type PAMConfig struct {

// UaccMetadata contains information the child needs from the parent for user accounting.
type UaccMetadata struct {
// The hostname of the node.
Hostname string `json:"hostname"`

// RemoteAddr is the address of the remote host.
RemoteAddr [4]int32 `json:"remote_addr"`
RemoteAddr utils.NetAddr `json:"remote_addr"`

// UtmpPath is the path of the system utmp database.
UtmpPath string `json:"utmp_path,omitempty"`
Expand All @@ -196,6 +193,9 @@ type UaccMetadata struct {

// BtmpPath is the path of the system btmp log.
BtmpPath string `json:"btmp_path,omitempty"`

// WtmpdbPath is the path of the system wtmpdb database.
WtmpdbPath string `json:"wtmpdb_path,omitempty"`
}

// RunCommand reads in the command to run from the parent process (over a
Expand Down Expand Up @@ -280,7 +280,6 @@ func RunCommand() (errw io.Writer, code int, err error) {
}()

var tty *os.File
uaccEnabled := false

// If a terminal was requested, file descriptor 7 always points to the
// TTY. Extract it and set the controlling TTY. Otherwise, connect
Expand Down Expand Up @@ -343,24 +342,34 @@ func RunCommand() (errw io.Writer, code int, err error) {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
readyfd = nil
uaccHandler := uacc.NewUserAccountHandler(uacc.UaccConfig{
UtmpFile: c.UaccMetadata.UtmpPath,
WtmpFile: c.UaccMetadata.WtmpPath,
BtmpFile: c.UaccMetadata.BtmpPath,
WtmpdbFile: c.UaccMetadata.WtmpdbPath,
})

localUser, err := user.Lookup(c.Login)
if err != nil {
if uaccErr := uacc.LogFailedLogin(c.UaccMetadata.BtmpPath, c.Login, c.UaccMetadata.Hostname, c.UaccMetadata.RemoteAddr); uaccErr != nil {
slog.DebugContext(ctx, "uacc unsupported", "error", uaccErr)
if uaccErr := uaccHandler.FailedLogin(c.Login, &c.UaccMetadata.RemoteAddr); uaccErr != nil {
slog.DebugContext(ctx, "unable to write failed login attempt to uacc", "error", uaccErr)
}
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}

if c.Terminal {
err = uacc.Open(c.UaccMetadata.UtmpPath, c.UaccMetadata.WtmpPath, c.Login, c.UaccMetadata.Hostname, c.UaccMetadata.RemoteAddr, tty)
// uacc support is best-effort, only enable it if Open is successful.
// Currently, there is no way to log this error out-of-band with the
// command output, so for now we essentially ignore it.
uaccSession, err := uaccHandler.OpenSession(tty, c.Login, &c.UaccMetadata.RemoteAddr)
if err == nil {
uaccEnabled = true
defer func() {
if closeErr := uaccSession.Close(); closeErr != nil {
slog.DebugContext(ctx, "failed to close uacc session", "error", closeErr)
}
}()
} else {
slog.DebugContext(ctx, "uacc unsupported", "error", err)
// uacc support is best-effort, only enable it if OpenSession is successful.
// Currently, there is no way to log this error out-of-band with the
// command output, so for now we essentially ignore it.
slog.DebugContext(ctx, "failed to open uacc session", "error", err)
}
}

Expand Down Expand Up @@ -430,13 +439,6 @@ func RunCommand() (errw io.Writer, code int, err error) {

err = waitForShell(terminatefd, cmd)

if uaccEnabled {
uaccErr := uacc.Close(c.UaccMetadata.UtmpPath, c.UaccMetadata.WtmpPath, tty)
if uaccErr != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(uaccErr)
}
}

return errorWriter, exitCode(err), trace.Wrap(err)
}

Expand Down
10 changes: 7 additions & 3 deletions lib/srv/regular/sshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ type Server struct {
// btmpPath is the path to the user accounting failed login log.
btmpPath string

// wtmpdbPath is the path to the wtmpdb database file.
wtmpdbPath string

// allowTCPForwarding indicates whether the ssh server is allowed to offer
// TCP port forwarding.
allowTCPForwarding bool
Expand Down Expand Up @@ -276,8 +279,8 @@ func (s *Server) GetAccessPoint() srv.AccessPoint {
}

// GetUserAccountingPaths returns the optional override of the utmp, wtmp, and btmp paths.
func (s *Server) GetUserAccountingPaths() (string, string, string) {
return s.utmpPath, s.wtmpPath, s.btmpPath
func (s *Server) GetUserAccountingPaths() (utmp string, wtmp string, btmp string, wtmpdb string) {
return s.utmpPath, s.wtmpPath, s.btmpPath, s.wtmpdbPath
}

// GetPAM returns the PAM configuration for this server.
Expand Down Expand Up @@ -441,11 +444,12 @@ func (s *Server) HandleConnection(conn net.Conn) {
}

// SetUserAccountingPaths is a functional server option to override the user accounting database and log path.
func SetUserAccountingPaths(utmpPath, wtmpPath, btmpPath string) ServerOption {
func SetUserAccountingPaths(utmpPath, wtmpPath, btmpPath, wtmpdbPath string) ServerOption {
return func(s *Server) error {
s.utmpPath = utmpPath
s.wtmpPath = wtmpPath
s.btmpPath = btmpPath
s.wtmpdbPath = wtmpdbPath
return nil
}
}
Expand Down
Loading
Loading