Skip to content

Commit

Permalink
Apply static host users to nodes
Browse files Browse the repository at this point in the history
This change adds functionality to create host users on a node from
matching static host user resources.
  • Loading branch information
atburke committed Sep 11, 2024
1 parent 4c1b8bc commit 87e95ec
Show file tree
Hide file tree
Showing 13 changed files with 744 additions and 214 deletions.
3 changes: 3 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,9 @@ const (
// provisioning system get added to when provisioned in KEEP mode. This prevents
// already existing users from being tampered with or deleted.
TeleportKeepGroup = "teleport-keep"
// TeleportStaticGroup is a default group that static host users get added to. This
// prevents already existing users from being tampered with or deleted.
TeleportStaticGroup = "teleport-static"
)

const (
Expand Down
225 changes: 189 additions & 36 deletions integration/hostuser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ import (
"github.com/google/uuid"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

labelv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/label/v1"
userprovisioningpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/userprovisioning"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/lite"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/srv"
Expand All @@ -63,7 +68,7 @@ func TestRootHostUsersBackend(t *testing.T) {
}

t.Run("Test CreateGroup", func(t *testing.T) {
t.Cleanup(cleanupUsersAndGroups(nil, []string{testgroup}))
t.Cleanup(func() { cleanupUsersAndGroups(nil, []string{testgroup}) })

err := usersbk.CreateGroup(testgroup, "")
require.NoError(t, err)
Expand All @@ -75,7 +80,7 @@ func TestRootHostUsersBackend(t *testing.T) {
})

t.Run("Test CreateUser and group", func(t *testing.T) {
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, []string{testgroup}))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, []string{testgroup}) })
err := usersbk.CreateGroup(testgroup, "")
require.NoError(t, err)

Expand Down Expand Up @@ -106,7 +111,7 @@ func TestRootHostUsersBackend(t *testing.T) {
})

t.Run("Test DeleteUser", func(t *testing.T) {
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, nil))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, nil) })
err := usersbk.CreateUser(testuser, nil, "", "", "")
require.NoError(t, err)
_, err = usersbk.Lookup(testuser)
Expand All @@ -121,7 +126,7 @@ func TestRootHostUsersBackend(t *testing.T) {

t.Run("Test GetAllUsers", func(t *testing.T) {
checkUsers := []string{"teleport-usera", "teleport-userb", "teleport-userc"}
t.Cleanup(cleanupUsersAndGroups(checkUsers, nil))
t.Cleanup(func() { cleanupUsersAndGroups(checkUsers, nil) })

for _, u := range checkUsers {
err := usersbk.CreateUser(u, []string{}, "", "", "")
Expand Down Expand Up @@ -153,7 +158,7 @@ func TestRootHostUsersBackend(t *testing.T) {
})

t.Run("Test CreateHomeDirectory does not follow symlinks", func(t *testing.T) {
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, nil))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, nil) })
err := usersbk.CreateUser(testuser, nil, "", "", "")
require.NoError(t, err)

Expand Down Expand Up @@ -196,19 +201,21 @@ func requireUserInGroups(t *testing.T, u *user.User, requiredGroups []string) {
require.Subset(t, getUserGroups(t, u), requiredGroups)
}

func cleanupUsersAndGroups(users []string, groups []string) func() {
return func() {
for _, group := range groups {
cmd := exec.Command("groupdel", group)
err := cmd.Run()
if err != nil {
log.Debugf("Error deleting group %s: %s", group, err)
}
}
for _, user := range users {
host.UserDel(user)
func cleanupUsersAndGroups(users []string, groups []string) {
for _, group := range groups {
cmd := exec.Command("groupdel", group)
err := cmd.Run()
if err != nil {
log.Debugf("Error deleting group %s: %s", group, err)
}
}
for _, user := range users {
host.UserDel(user)
}
}

func sudoersPath(username, uuid string) string {
return fmt.Sprintf("/etc/sudoers.d/teleport-%s-%s", uuid, username)
}

func TestRootHostUsers(t *testing.T) {
Expand All @@ -223,11 +230,11 @@ func TestRootHostUsers(t *testing.T) {
users := srv.NewHostUsers(context.Background(), presence, "host_uuid")

testGroups := []string{"group1", "group2"}
closer, err := users.UpsertUser(testuser, services.HostUsersInfo{Groups: testGroups, Mode: types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP})
closer, err := users.UpsertUser(testuser, services.HostUsersInfo{Groups: testGroups, Mode: services.HostUserModeDrop})
require.NoError(t, err)

testGroups = append(testGroups, types.TeleportDropGroup)
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, testGroups))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, testGroups) })

u, err := user.Lookup(testuser)
require.NoError(t, err)
Expand All @@ -249,13 +256,13 @@ func TestRootHostUsers(t *testing.T) {
require.ErrorIs(t, err, user.UnknownGroupIdError(testGID))

closer, err := users.UpsertUser(testuser, services.HostUsersInfo{
Mode: types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP,
Mode: services.HostUserModeDrop,
UID: testUID,
GID: testGID,
})
require.NoError(t, err)

t.Cleanup(cleanupUsersAndGroups([]string{testuser}, []string{types.TeleportDropGroup}))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, []string{types.TeleportDropGroup}) })

group, err := user.LookupGroupId(testGID)
require.NoError(t, err)
Expand All @@ -277,10 +284,10 @@ func TestRootHostUsers(t *testing.T) {
expectedHome := filepath.Join("/home", testuser)
require.NoDirExists(t, expectedHome)

closer, err := users.UpsertUser(testuser, services.HostUsersInfo{Mode: types.CreateHostUserMode_HOST_USER_MODE_KEEP})
closer, err := users.UpsertUser(testuser, services.HostUsersInfo{Mode: services.HostUserModeKeep})
require.NoError(t, err)
require.Nil(t, closer)
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, []string{types.TeleportKeepGroup}))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, []string{types.TeleportKeepGroup}) })

u, err := user.Lookup(testuser)
require.NoError(t, err)
Expand All @@ -299,17 +306,13 @@ func TestRootHostUsers(t *testing.T) {
users := srv.NewHostUsers(context.Background(), presence, uuid)
sudoers := srv.NewHostSudoers(uuid)

sudoersPath := func(username, uuid string) string {
return fmt.Sprintf("/etc/sudoers.d/teleport-%s-%s", uuid, username)
}

t.Cleanup(func() {
os.Remove(sudoersPath(testuser, uuid))
host.UserDel(testuser)
})
closer, err := users.UpsertUser(testuser,
services.HostUsersInfo{
Mode: types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP,
Mode: services.HostUserModeDrop,
})
require.NoError(t, err)
err = sudoers.WriteSudoers(testuser, []string{"ALL=(ALL) ALL"})
Expand Down Expand Up @@ -338,20 +341,22 @@ func TestRootHostUsers(t *testing.T) {

deleteableUsers := []string{"teleport-user1", "teleport-user2", "teleport-user3"}
for _, user := range deleteableUsers {
_, err := users.UpsertUser(user, services.HostUsersInfo{Mode: types.CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP})
_, err := users.UpsertUser(user, services.HostUsersInfo{Mode: services.HostUserModeDrop})
require.NoError(t, err)
}

// this user should not be in the service group as it was created with mode keep.
closer, err := users.UpsertUser("teleport-user4", services.HostUsersInfo{
Mode: types.CreateHostUserMode_HOST_USER_MODE_KEEP,
Mode: services.HostUserModeKeep,
})
require.NoError(t, err)
require.Nil(t, closer)

t.Cleanup(cleanupUsersAndGroups(
[]string{"teleport-user1", "teleport-user2", "teleport-user3", "teleport-user4"},
[]string{types.TeleportDropGroup, types.TeleportKeepGroup}))
t.Cleanup(func() {
cleanupUsersAndGroups(
[]string{"teleport-user1", "teleport-user2", "teleport-user3", "teleport-user4"},
[]string{types.TeleportDropGroup, types.TeleportKeepGroup})
})

err = users.DeleteAllUsers()
require.NoError(t, err)
Expand Down Expand Up @@ -393,13 +398,13 @@ func TestRootHostUsers(t *testing.T) {

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(cleanupUsersAndGroups([]string{testuser}, slices.Concat(tc.firstGroups, tc.secondGroups)))
t.Cleanup(func() { cleanupUsersAndGroups([]string{testuser}, slices.Concat(tc.firstGroups, tc.secondGroups)) })

// Verify that the user is created with the first set of groups.
users := srv.NewHostUsers(context.Background(), presence, "host_uuid")
_, err := users.UpsertUser(testuser, services.HostUsersInfo{
Groups: tc.firstGroups,
Mode: types.CreateHostUserMode_HOST_USER_MODE_KEEP,
Mode: services.HostUserModeKeep,
})
require.NoError(t, err)
u, err := user.Lookup(testuser)
Expand All @@ -409,7 +414,7 @@ func TestRootHostUsers(t *testing.T) {
// Verify that the user is updated with the second set of groups.
_, err = users.UpsertUser(testuser, services.HostUsersInfo{
Groups: tc.secondGroups,
Mode: types.CreateHostUserMode_HOST_USER_MODE_KEEP,
Mode: services.HostUserModeKeep,
})
require.NoError(t, err)
u, err = user.Lookup(testuser)
Expand Down Expand Up @@ -505,7 +510,7 @@ func TestRootLoginAsHostUser(t *testing.T) {
require.NoError(t, err)

// Run an SSH session to completion.
t.Cleanup(cleanupUsersAndGroups([]string{login}, groups))
t.Cleanup(func() { cleanupUsersAndGroups([]string{login}, groups) })
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
err = client.SSH(ctx, tc.command)
Expand All @@ -525,3 +530,151 @@ func TestRootLoginAsHostUser(t *testing.T) {
})
}
}

func TestRootStaticHostUsers(t *testing.T) {
utils.RequireRoot(t)
// Create test instance.
privateKey, publicKey, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)

instance := helpers.NewInstance(t, helpers.InstanceConfig{
ClusterName: helpers.Site,
HostID: uuid.New().String(),
NodeName: Host,
Priv: privateKey,
Pub: publicKey,
Log: utils.NewLoggerForTests(),
})

require.NoError(t, instance.Create(t, nil, false, nil))
require.NoError(t, instance.Start())
t.Cleanup(func() {
require.NoError(t, instance.StopAll())
})
nodeCfg := servicecfg.MakeDefaultConfig()
nodeCfg.SSH.Labels = map[string]string{
"foo": "bar",
}
_, err = instance.StartNode(nodeCfg)
require.NoError(t, err)

// Create host user resources.
groups := []string{"foo", "bar"}
goodLogin := utils.GenerateLocalUsername(t)
goodUser := userprovisioning.NewStaticHostUser(goodLogin, &userprovisioningpb.StaticHostUserSpec{
Matchers: []*userprovisioningpb.Matcher{
{
NodeLabels: []*labelv1.Label{
{
Name: "foo",
Values: []string{"bar"},
},
},
Groups: groups,
Sudoers: []string{"All = (root) NOPASSWD: /usr/bin/systemctl restart nginx.service"},
},
},
})
nonMatchingLogin := utils.GenerateLocalUsername(t)
nonMatchingUser := userprovisioning.NewStaticHostUser(nonMatchingLogin, &userprovisioningpb.StaticHostUserSpec{
Matchers: []*userprovisioningpb.Matcher{
{
NodeLabels: []*labelv1.Label{
{
Name: "foo",
Values: []string{"baz"},
},
},
Groups: groups,
},
},
})
conflictingLogin := utils.GenerateLocalUsername(t)
conflictingUser := userprovisioning.NewStaticHostUser(conflictingLogin, &userprovisioningpb.StaticHostUserSpec{
Matchers: []*userprovisioningpb.Matcher{
{
NodeLabels: []*labelv1.Label{
{
Name: "foo",
Values: []string{"bar"},
},
},
Groups: groups,
},
{
NodeLabelsExpression: `labels["foo"] == "bar"`,
Groups: groups,
},
},
})

clt := instance.Process.GetAuthServer()
for _, hostUser := range []*userprovisioningpb.StaticHostUser{goodUser, nonMatchingUser, conflictingUser} {
_, err := clt.UpsertStaticHostUser(context.Background(), hostUser)
require.NoError(t, err)
}
t.Cleanup(func() { cleanupUsersAndGroups([]string{goodLogin, nonMatchingLogin, conflictingLogin}, groups) })

// Test that a node picks up new host users from the cache.
testStaticHostUsers(t, nodeCfg.HostUUID, goodLogin, nonMatchingLogin, conflictingLogin, groups)
cleanupUsersAndGroups([]string{goodLogin, nonMatchingLogin, conflictingLogin}, groups)

require.NoError(t, instance.StopNodes())
_, err = instance.StartNode(nodeCfg)
require.NoError(t, err)
// Test that a new node picks up existing host users on startup.
testStaticHostUsers(t, nodeCfg.HostUUID, goodLogin, nonMatchingLogin, conflictingLogin, groups)

// Check that a deleted resource doesn't affect the host user.
require.NoError(t, clt.DeleteStaticHostUser(context.Background(), goodLogin))
var lookupErr error
var homeDirErr error
var sudoerErr error
require.Never(t, func() bool {
_, lookupErr = user.Lookup(goodLogin)
_, homeDirErr = os.Stat("/home/" + goodLogin)
_, sudoerErr = os.Stat(sudoersPath(goodLogin, nodeCfg.HostUUID))
return lookupErr != nil || homeDirErr != nil || sudoerErr != nil
}, 5*time.Second, time.Second,
"lookup err: %v\nhome dir err: %v\nsudoer err: %v\n",
lookupErr, homeDirErr, sudoerErr)
}

func testStaticHostUsers(t *testing.T, nodeUUID, goodLogin, nonMatchingLogin, conflictingLogin string, groups []string) {
t.Cleanup(func() {
os.Remove(sudoersPath(goodLogin, nodeUUID))
})

// Check that the good user was correctly applied.
require.EventuallyWithT(t, func(collect *assert.CollectT) {
// Check that the user was created.
existingUser, err := user.Lookup(goodLogin)
assert.NoError(collect, err)
assert.DirExists(collect, existingUser.HomeDir)
// Check that the user has the right groups, including teleport-static.
groupIDs, err := existingUser.GroupIds()
assert.NoError(collect, err)
userGroups := make([]string, 0, len(groupIDs))
for _, gid := range groupIDs {
group, err := user.LookupGroupId(gid)
assert.NoError(collect, err)
userGroups = append(userGroups, group.Name)
}
assert.Subset(collect, userGroups, groups)
assert.Contains(collect, userGroups, types.TeleportStaticGroup)
// Check that the sudoers file was created.
assert.FileExists(collect, sudoersPath(goodLogin, nodeUUID))
}, 10*time.Second, time.Second)

// Check that the nonmatching and conflicting users were not created.
var nonmatchingUserErr error
var conflictingUserErr error
require.Never(t, func() bool {
_, nonmatchingUserErr = user.Lookup(nonMatchingLogin)
_, conflictingUserErr = user.Lookup(conflictingLogin)
return nonmatchingUserErr == nil && conflictingUserErr == nil
}, 5*time.Second, time.Second,
"nonmatching user error: %v\nconflicting user error: %v\n",
nonmatchingUserErr, conflictingUserErr,
)
}
1 change: 1 addition & 0 deletions lib/authz/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionReco
types.NewRule(types.KindLock, services.RO()),
types.NewRule(types.KindNetworkRestrictions, services.RO()),
types.NewRule(types.KindConnectionDiagnostic, services.RW()),
types.NewRule(types.KindStaticHostUser, services.RO()),
},
},
})
Expand Down
Loading

0 comments on commit 87e95ec

Please sign in to comment.