Merge pull request #20073 from hashicorp/feat/uid-gid-restriction

Adds ability to restrict uid and gids in exec and raw_exec
This commit is contained in:
Juana De La Cuesta
2024-10-31 15:48:45 +01:00
committed by GitHub
15 changed files with 685 additions and 16 deletions

3
.changelog/20073.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
drivers: add posibility to restrict user and group for exec and rawexec
```

View File

@@ -14,7 +14,7 @@ type (
// Must be an alias because go-msgpack cannot handle the real type.
NodeID = uint8
// A SocketID represents a physicsl CPU socket.
// A SocketID represents a physical CPU socket.
SocketID uint8
// A CoreID represents one logical (vCPU) core.

View File

@@ -20,6 +20,7 @@ import (
"github.com/hashicorp/nomad/drivers/shared/eventer"
"github.com/hashicorp/nomad/drivers/shared/executor"
"github.com/hashicorp/nomad/drivers/shared/resolvconf"
"github.com/hashicorp/nomad/drivers/shared/validators"
"github.com/hashicorp/nomad/helper/pluginutils/loader"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/plugins/base"
@@ -83,6 +84,8 @@ var (
hclspec.NewAttr("allow_caps", "list(string)", false),
hclspec.NewLiteral(capabilities.HCLSpecLiteral),
),
"denied_host_uids": hclspec.NewAttr("denied_host_uids", "string", false),
"denied_host_gids": hclspec.NewAttr("denied_host_gids", "string", false),
})
// taskConfigSpec is the hcl specification for the driver config section of
@@ -140,6 +143,8 @@ type Driver struct {
// compute contains cpu compute information
compute cpustats.Compute
userIDValidator UserIDValidator
}
// Config is the driver configuration set by the SetConfig RPC call
@@ -159,6 +164,9 @@ type Config struct {
// AllowCaps configures which Linux Capabilities are enabled for tasks
// running on this node.
AllowCaps []string `codec:"allow_caps"`
DeniedHostUids string `codec:"denied_host_uids"`
DeniedHostGids string `codec:"denied_host_gids"`
}
func (c *Config) validate() error {
@@ -223,6 +231,7 @@ func (tc *TaskConfig) validate() error {
if !badAdds.Empty() {
return fmt.Errorf("cap_add configured with capabilities not supported by system: %s", badAdds)
}
badDrops := supported.Difference(capabilities.New(tc.CapDrop))
if !badDrops.Empty() {
return fmt.Errorf("cap_drop configured with capabilities not supported by system: %s", badDrops)
@@ -241,6 +250,10 @@ type TaskState struct {
StartedAt time.Time
}
type UserIDValidator interface {
HasValidIDs(userName string) error
}
// NewExecDriver returns a new DrivePlugin implementation
func NewExecDriver(ctx context.Context, logger hclog.Logger) drivers.DriverPlugin {
logger = logger.Named(pluginName)
@@ -285,14 +298,26 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) {
func (d *Driver) SetConfig(cfg *base.Config) error {
// unpack, validate, and set agent plugin config
var config Config
if len(cfg.PluginConfig) != 0 {
if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil {
return err
}
}
if err := config.validate(); err != nil {
return err
}
if d.userIDValidator == nil {
idValidator, err := validators.NewValidator(d.logger, config.DeniedHostUids, config.DeniedHostGids)
if err != nil {
return fmt.Errorf("unable to start validator: %w", err)
}
d.userIDValidator = idValidator
}
d.config = config
if cfg != nil && cfg.AgentConfig != nil {
@@ -437,6 +462,16 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
return nil, nil, fmt.Errorf("failed driver config validation: %v", err)
}
if cfg.User == "" {
cfg.User = "nobody"
}
d.logger.Debug("setting up user", "user", cfg.User)
if err := d.userIDValidator.HasValidIDs(cfg.User); err != nil {
return nil, nil, fmt.Errorf("failed host user validation: %v", err)
}
d.logger.Info("starting task", "driver_cfg", hclog.Fmt("%+v", driverConfig))
handle := drivers.NewTaskHandle(taskHandleVersion)
handle.Config = cfg
@@ -457,10 +492,6 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
}
user := cfg.User
if user == "" {
user = "nobody"
}
if cfg.DNS != nil {
dnsMount, err := resolvconf.GenerateDNSMount(cfg.TaskDir().Dir, cfg.DNS)
if err != nil {

View File

@@ -36,6 +36,12 @@ import (
"github.com/stretchr/testify/require"
)
type mockIDValidator struct{}
func (mv *mockIDValidator) HasValidIDs(userName string) error {
return nil
}
func TestMain(m *testing.M) {
if !testtask.Run() {
os.Exit(m.Run())
@@ -70,6 +76,8 @@ func newExecDriverTest(t *testing.T, ctx context.Context) drivers.DriverPlugin {
topology := numalib.Scan(numalib.PlatformScanners())
d := NewExecDriver(ctx, testlog.HCLogger(t))
d.(*Driver).nomadConfig = &base.ClientDriverConfig{Topology: topology}
d.(*Driver).userIDValidator = &mockIDValidator{}
return d
}
@@ -831,6 +839,81 @@ func TestExecDriver_OOMKilled(t *testing.T) {
must.NoError(t, harness.DestroyTask(task.ID, true))
}
func TestDriver_Config_setDeniedIds(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
uidRanges string
gidRanges string
exError bool
}{
{
name: "empty_ranges",
uidRanges: "",
gidRanges: "",
exError: false,
},
{
name: "valid_ranges",
uidRanges: "1-10",
gidRanges: "1-10",
exError: false,
},
{
name: "empty_GID_invalid_UID_range",
uidRanges: "10-1",
gidRanges: "",
exError: true,
},
{
name: "empty_UID_invalid_GID_range",
uidRanges: "",
gidRanges: "10-1",
exError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := newExecDriverTest(t, ctx)
// Force the creation of the validatior.
d.(*Driver).userIDValidator = nil
harness := dtestutil.NewDriverHarness(t, d)
defer harness.Kill()
config := &Config{
NoPivotRoot: false,
DefaultModePID: executor.IsolationModePrivate,
DefaultModeIPC: executor.IsolationModePrivate,
DeniedHostUids: tc.uidRanges,
DeniedHostGids: tc.gidRanges,
}
var data []byte
must.NoError(t, base.MsgPackEncode(&data, config))
baseConfig := &base.Config{
PluginConfig: data,
AgentConfig: &base.AgentConfig{
Driver: &base.ClientDriverConfig{
Topology: d.(*Driver).nomadConfig.Topology,
},
},
}
err := harness.SetConfig(baseConfig)
must.Eq(t, err != nil, tc.exError)
})
}
}
func TestDriver_Config_validate(t *testing.T) {
ci.Parallel(t)
t.Run("pid/ipc", func(t *testing.T) {
@@ -874,6 +957,7 @@ func TestDriver_Config_validate(t *testing.T) {
func TestDriver_TaskConfig_validate(t *testing.T) {
ci.Parallel(t)
t.Run("pid/ipc", func(t *testing.T) {
for _, tc := range []struct {
pidMode, ipcMode string
@@ -889,7 +973,7 @@ func TestDriver_TaskConfig_validate(t *testing.T) {
{pidMode: "", ipcMode: "host", exp: nil},
{pidMode: "other", ipcMode: "host", exp: errors.New(`pid_mode must be "private" or "host", got "other"`)},
} {
require.Equal(t, tc.exp, (&TaskConfig{
must.Eq(t, tc.exp, (&TaskConfig{
ModePID: tc.pidMode,
ModeIPC: tc.ipcMode,
}).validate())
@@ -907,7 +991,7 @@ func TestDriver_TaskConfig_validate(t *testing.T) {
{adds: []string{"chown", "sys_time"}, exp: nil},
{adds: []string{"chown", "not_valid", "sys_time"}, exp: errors.New("cap_add configured with capabilities not supported by system: not_valid")},
} {
require.Equal(t, tc.exp, (&TaskConfig{
must.Eq(t, tc.exp, (&TaskConfig{
CapAdd: tc.adds,
}).validate())
}
@@ -924,7 +1008,7 @@ func TestDriver_TaskConfig_validate(t *testing.T) {
{drops: []string{"chown", "sys_time"}, exp: nil},
{drops: []string{"chown", "not_valid", "sys_time"}, exp: errors.New("cap_drop configured with capabilities not supported by system: not_valid")},
} {
require.Equal(t, tc.exp, (&TaskConfig{
must.Eq(t, tc.exp, (&TaskConfig{
CapDrop: tc.drops,
}).validate())
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/hashicorp/nomad/client/lib/cpustats"
"github.com/hashicorp/nomad/drivers/shared/eventer"
"github.com/hashicorp/nomad/drivers/shared/executor"
"github.com/hashicorp/nomad/drivers/shared/validators"
"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
"github.com/hashicorp/nomad/helper/pluginutils/loader"
"github.com/hashicorp/nomad/plugins/base"
@@ -81,6 +82,8 @@ var (
hclspec.NewAttr("enabled", "bool", false),
hclspec.NewLiteral("false"),
),
"denied_host_uids": hclspec.NewAttr("denied_host_uids", "string", false),
"denied_host_gids": hclspec.NewAttr("denied_host_gids", "string", false),
})
// taskConfigSpec is the hcl specification for the driver config section of
@@ -107,6 +110,10 @@ var (
}
)
type UserIDValidator interface {
HasValidIDs(userName string) error
}
// Driver is a privileged version of the exec driver. It provides no
// resource isolation and just fork/execs. The Exec driver should be preferred
// and this should only be used when explicitly needed.
@@ -133,12 +140,17 @@ type Driver struct {
// compute contains cpu compute information
compute cpustats.Compute
userIDValidator UserIDValidator
}
// Config is the driver configuration set by the SetConfig RPC call
type Config struct {
// Enabled is set to true to enable the raw_exec driver
Enabled bool `codec:"enabled"`
DeniedHostUids string `codec:"denied_host_uids"`
DeniedHostGids string `codec:"denied_host_gids"`
}
// TaskConfig is the driver configuration of a task within a job
@@ -194,17 +206,29 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) {
func (d *Driver) SetConfig(cfg *base.Config) error {
var config Config
if len(cfg.PluginConfig) != 0 {
if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil {
return err
}
}
if d.userIDValidator == nil {
idValidator, err := validators.NewValidator(d.logger, config.DeniedHostUids, config.DeniedHostGids)
if err != nil {
return fmt.Errorf("unable to start validator: %w", err)
}
d.userIDValidator = idValidator
}
d.config = &config
if cfg.AgentConfig != nil {
d.nomadConfig = cfg.AgentConfig.Driver
d.compute = cfg.AgentConfig.Compute()
}
return nil
}
@@ -332,6 +356,10 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
return nil, nil, fmt.Errorf("oom_score_adj must not be negative")
}
if err := d.Validate(*cfg); err != nil {
return nil, nil, fmt.Errorf("failed driver config validation: %v", err)
}
d.logger.Info("starting task", "driver_cfg", hclog.Fmt("%+v", driverConfig))
handle := drivers.NewTaskHandle(taskHandleVersion)
handle.Config = cfg

View File

@@ -76,6 +76,12 @@ var (
topology = numalib.Scan(numalib.PlatformScanners())
)
type mockIDValidator struct{}
func (mv *mockIDValidator) HasValidIDs(userName string) error {
return nil
}
func newEnabledRawExecDriver(t *testing.T) *Driver {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
@@ -86,13 +92,13 @@ func newEnabledRawExecDriver(t *testing.T) *Driver {
d.nomadConfig = &base.ClientDriverConfig{
Topology: topology,
}
d.userIDValidator = &mockIDValidator{}
return d
}
func TestRawExecDriver_SetConfig(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -110,18 +116,35 @@ func TestRawExecDriver_SetConfig(t *testing.T) {
)
// Default is raw_exec is disabled.
require.NoError(basePlug.MsgPackEncode(&data, config))
must.NoError(t, basePlug.MsgPackEncode(&data, config))
bconfig.PluginConfig = data
require.NoError(harness.SetConfig(bconfig))
require.Exactly(config, d.(*Driver).config)
must.NoError(t, harness.SetConfig(bconfig))
must.Eq(t, config, d.(*Driver).config)
// Enable raw_exec, but disable cgroups.
config.Enabled = true
data = []byte{}
require.NoError(basePlug.MsgPackEncode(&data, config))
must.NoError(t, basePlug.MsgPackEncode(&data, config))
bconfig.PluginConfig = data
require.NoError(harness.SetConfig(bconfig))
require.Exactly(config, d.(*Driver).config)
must.NoError(t, harness.SetConfig(bconfig))
must.Eq(t, config, d.(*Driver).config)
// Turns on uid/gid restrictions, and sets the range to a bad value and
// force the recreation of the validator.
d.(*Driver).userIDValidator = nil
config.DeniedHostUids = "100-1"
data = []byte{}
must.NoError(t, basePlug.MsgPackEncode(&data, config))
bconfig.PluginConfig = data
err := harness.SetConfig(bconfig)
must.Error(t, err)
must.ErrorContains(t, err, "invalid range deniedHostUIDs \"100-1\": lower bound cannot be greater than upper bound")
}
func TestRawExecDriver_Fingerprint(t *testing.T) {
@@ -209,6 +232,7 @@ func TestRawExecDriver_StartWait(t *testing.T) {
Args: []string{"sleep", "10ms"},
}
require.NoError(task.EncodeConcreteDriverConfig(&tc))
testtask.SetTaskConfigEnv(task)
cleanup := harness.MkAllocDir(task, false)

View File

@@ -0,0 +1,30 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package rawexec
import (
"fmt"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/plugins/drivers"
)
func (d *Driver) Validate(cfg drivers.TaskConfig) error {
usernameToLookup := cfg.User
// Uses the current user of the client agent process
// if no override is given (differs from exec)
if usernameToLookup == "" {
user, err := users.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
usernameToLookup = user.Username
}
return d.userIDValidator.HasValidIDs(usernameToLookup)
}

View File

@@ -7,6 +7,7 @@ package rawexec
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
@@ -22,6 +23,7 @@ import (
"github.com/hashicorp/nomad/ci"
clienttestutil "github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/helper/testtask"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/plugins/base"
basePlug "github.com/hashicorp/nomad/plugins/base"
@@ -456,6 +458,7 @@ func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) {
config := &Config{Enabled: true}
var data []byte
require.NoError(basePlug.MsgPackEncode(&data, config))
bconfig := &basePlug.Config{
PluginConfig: data,
@@ -476,6 +479,7 @@ func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) {
Env: defaultEnv(),
Resources: testResources(allocID, taskName),
}
tc := &TaskConfig{
Command: testtask.Path(),
Args: []string{"sleep", "100s"},
@@ -542,3 +546,69 @@ func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) {
require.NoError(d.DestroyTask(task.ID, false))
require.True(waitDone)
}
func TestRawExec_Validate(t *testing.T) {
ci.Parallel(t)
current, err := users.Current()
must.NoError(t, err)
currentUserErrStr := fmt.Sprintf("running as uid %s is disallowed", current.Uid)
allowAll := ""
denyCurrent := current.Uid
configAllowCurrent := Config{DeniedHostUids: allowAll}
configDenyCurrent := Config{DeniedHostUids: denyCurrent}
driverConfigNoUserSpecified := drivers.TaskConfig{}
driverTaskConfig := drivers.TaskConfig{User: current.Name}
for _, tc := range []struct {
config Config
driverConfig drivers.TaskConfig
exp error
}{
{
config: configAllowCurrent,
driverConfig: driverTaskConfig,
exp: nil,
},
{
config: configDenyCurrent,
driverConfig: driverConfigNoUserSpecified,
exp: errors.New(currentUserErrStr),
},
{
config: configDenyCurrent,
driverConfig: driverTaskConfig,
exp: errors.New(currentUserErrStr),
},
} {
d := newEnabledRawExecDriver(t)
// Force the creation of the validatior, the mock is used by newEnabledRawExecDriver by default
d.userIDValidator = nil
harness := dtestutil.NewDriverHarness(t, d)
defer harness.Kill()
config := tc.config
var data []byte
must.NoError(t, base.MsgPackEncode(&data, config))
bconfig := &base.Config{
PluginConfig: data,
AgentConfig: &base.AgentConfig{
Driver: &base.ClientDriverConfig{
Topology: d.nomadConfig.Topology,
},
},
}
must.NoError(t, harness.SetConfig(bconfig))
must.Eq(t, tc.exp, d.Validate(tc.driverConfig))
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build windows
package rawexec
import (
"github.com/hashicorp/nomad/plugins/drivers"
)
func (d *Driver) Validate(cfg drivers.TaskConfig) error {
// This is a noop on windows since the uid and gid cannot be checked against a range easily
// We could eventually extend this functionality to check for individual users IDs strings
// but that is not currently supported. See driverValidators.HasValidIds for
// unix logic
return nil
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package validators
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/lib/idset"
"github.com/hashicorp/nomad/helper/users"
)
var (
ErrInvalidBound = errors.New("range bound not valid")
//ErrEmptyRange = errors.New("range value cannot be empty")
ErrInvalidRange = errors.New("lower bound cannot be greater than upper bound")
)
type (
// A GroupID (GID) represents a unique numerical value assigned to each user group.
GroupID uint64
// A UserID represents a unique numerical value assigned to each user account.
UserID uint64
)
type Validator struct {
// DeniedHostUids configures which host uids are disallowed
deniedUIDs *idset.Set[UserID]
// DeniedHostGids configures which host gids are disallowed
deniedGIDs *idset.Set[GroupID]
// logger will log to the Nomad agent
logger hclog.Logger
}
func NewValidator(logger hclog.Logger, deniedHostUIDs, deniedHostGIDs string) (*Validator, error) {
valLogger := logger.Named("id_validator")
err := validateIDRange("deniedHostUIDs", deniedHostUIDs)
if err != nil {
return nil, err
}
valLogger.Debug("user range configured", "denied range", deniedHostUIDs)
err = validateIDRange("deniedHostGIDs", deniedHostGIDs)
if err != nil {
return nil, err
}
valLogger.Debug("group range configured", "denied range", deniedHostGIDs)
v := &Validator{
deniedUIDs: idset.Parse[UserID](deniedHostUIDs),
deniedGIDs: idset.Parse[GroupID](deniedHostGIDs),
logger: valLogger,
}
return v, nil
}
// HasValidIDs is used when running a task to ensure the
// given user is in the ID range defined in the task config
func (v *Validator) HasValidIDs(userName string) error {
user, err := users.Lookup(userName)
if err != nil {
return fmt.Errorf("failed to identify user %q: %w", userName, err)
}
uid, err := getUserID(user)
if err != nil {
return fmt.Errorf("validator: %w", err)
}
// check uids
if v.deniedUIDs.Contains(uid) {
return fmt.Errorf("running as uid %d is disallowed", uid)
}
gids, err := getGroupsID(user)
if err != nil {
return fmt.Errorf("validator: %w", err)
}
// check gids
for _, gid := range gids {
if v.deniedGIDs.Contains(gid) {
return fmt.Errorf("running as gid %d is disallowed", gid)
}
}
return nil
}
// validateIDRange is used to ensure that the configuration for ID ranges is valid
// by checking the syntax and bounds.
func validateIDRange(rangeType string, deniedRanges string) error {
parts := strings.Split(deniedRanges, ",")
// exit early if empty string
if len(parts) == 1 && parts[0] == "" {
return nil
}
for _, rangeStr := range parts {
err := validateBounds(rangeStr)
if err != nil {
return fmt.Errorf("invalid range %s \"%s\": %w", rangeType, rangeStr, err)
}
}
return nil
}
func validateBounds(boundsString string) error {
uidDenyRangeParts := strings.Split(boundsString, "-")
switch len(uidDenyRangeParts) {
case 1:
disallowedIdStr := uidDenyRangeParts[0]
if _, err := strconv.ParseUint(disallowedIdStr, 10, 32); err != nil {
return ErrInvalidBound
}
case 2:
lowerBoundStr := uidDenyRangeParts[0]
upperBoundStr := uidDenyRangeParts[1]
lowerBoundInt, err := strconv.ParseUint(lowerBoundStr, 10, 32)
if err != nil {
return ErrInvalidBound
}
upperBoundInt, err := strconv.ParseUint(upperBoundStr, 10, 32)
if err != nil {
return ErrInvalidBound
}
if lowerBoundInt > upperBoundInt {
return ErrInvalidRange
}
}
return nil
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build windows
package validators
import (
"os/user"
)
// noop
func getUserID(*user.User) (UserID, error) {
return 0, nil
}
// noop
func getGroupsID(*user.User) ([]GroupID, error) {
return []GroupID{}, nil
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !windows
package validators
import (
"fmt"
"os/user"
"strconv"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/shoenig/test/must"
)
var validRangeStr = "1-100"
var validRangeSingleStr = "1"
var flippedBoundsMessage = "lower bound cannot be greater than upper bound"
var invalidRangeFlipped = "100-1"
var invalidBound = "range bound not valid"
var invalidRangeSubstring = "1-100,foo"
var invalidRangeEmpty = "1-100,,200-300"
func Test_IDRangeValid(t *testing.T) {
testCases := []struct {
name string
idRange string
expectedErr string
}{
{name: "standard-range-is-valid", idRange: validRangeStr},
{name: "same-number-for-both-bounds-is-valid", idRange: validRangeSingleStr},
{name: "lower-higher-than-upper-is-invalid", idRange: invalidRangeFlipped, expectedErr: flippedBoundsMessage},
{name: "missing-lower-is-invalid", idRange: invalidRangeSubstring, expectedErr: invalidBound},
{name: "missing-higher-is-invalid", idRange: invalidRangeEmpty, expectedErr: invalidBound},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateIDRange("uid", tc.idRange)
if tc.expectedErr == "" {
must.NoError(t, err)
} else {
must.Error(t, err)
must.ErrorContains(t, err, tc.expectedErr)
}
})
}
}
func Test_HasValidIds(t *testing.T) {
user, err := user.Current()
must.NoError(t, err)
userID, err := strconv.ParseUint(user.Uid, 10, 32)
groupID, err := strconv.ParseUint(user.Gid, 10, 32)
must.NoError(t, err)
userNotIncluded := fmt.Sprintf("%d-%d", userID+1, userID+11)
userIncluded := fmt.Sprintf("%d-%d", userID, userID+11)
userNotIncludedSingle := fmt.Sprintf("%d", userID+1)
groupNotIncluded := fmt.Sprintf("%d-%d", groupID+1, groupID+11)
groupIncluded := fmt.Sprintf("%d-%d", groupID, groupID+11)
groupNotIncludedSingle := fmt.Sprintf("%d", groupID+1)
emptyRanges := ""
userDeniedRangesList := fmt.Sprintf("%s,%s", userNotIncluded, userNotIncludedSingle)
groupDeniedRangesList := fmt.Sprintf("%s,%s", groupNotIncluded, groupNotIncludedSingle)
testCases := []struct {
name string
uidRanges string
gidRanges string
expectedErr string
}{
{name: "user_not_in_denied_ranges", uidRanges: userDeniedRangesList, gidRanges: emptyRanges},
{name: "user_and group_not_in_denied_ranges", uidRanges: userDeniedRangesList, gidRanges: groupDeniedRangesList},
{name: "uid_in_one_of_ranges_is_invalid", uidRanges: userIncluded, gidRanges: groupDeniedRangesList, expectedErr: fmt.Sprintf("running as uid %s is disallowed", user.Uid)},
{name: "gid-in-one-of-ranges-is-invalid", uidRanges: userDeniedRangesList, gidRanges: groupIncluded, expectedErr: fmt.Sprintf("running as gid %s is disallowed", user.Gid)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
v, err := NewValidator(hclog.NewNullLogger(), tc.uidRanges, tc.gidRanges)
must.NoError(t, err)
err = v.HasValidIDs(user.Username)
if tc.expectedErr == "" {
must.NoError(t, err)
} else {
must.Error(t, err)
must.ErrorContains(t, err, tc.expectedErr)
}
})
}
}
func Test_ValidateBounds(t *testing.T) {
testCases := []struct {
name string
bounds string
expectedErr error
}{
{name: "invalid_bound", bounds: "banana", expectedErr: ErrInvalidBound},
{name: "invalid_lower_bound", bounds: "banana-10", expectedErr: ErrInvalidBound},
{name: "invalid_upper_bound", bounds: "10-banana", expectedErr: ErrInvalidBound},
{name: "lower_bigger_than_upper", bounds: "10-1", expectedErr: ErrInvalidRange},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateBounds(tc.bounds)
must.ErrorIs(t, err, tc.expectedErr)
})
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !windows
package validators
import (
"fmt"
"os/user"
"strconv"
)
func getUserID(user *user.User) (UserID, error) {
id, err := strconv.ParseUint(user.Uid, 10, 32)
if err != nil {
return 0, fmt.Errorf("unable to convert userid %s to integer", user.Uid)
}
return UserID(id), nil
}
func getGroupsID(user *user.User) ([]GroupID, error) {
gidStrings, err := user.GroupIds()
if err != nil {
return []GroupID{}, fmt.Errorf("unable to lookup user's group membership: %w", err)
}
gids := make([]GroupID, len(gidStrings))
for _, gidString := range gidStrings {
u, err := strconv.ParseUint(gidString, 10, 32)
if err != nil {
return []GroupID{}, fmt.Errorf("unable to convert user's group %q to integer: %w", gidString, err)
}
gids = append(gids, GroupID(u))
}
return gids, nil
}

View File

@@ -188,6 +188,30 @@ able to make use of IPC features, like sending unexpected POSIX signals.
undesirable consequences, including untrusted tasks being able to compromise the
host system.
- `denied_host_uids` - (Optional) Specifies a comma-separated list of host uids to
deny. Ranges can be specified by using a hyphen separating the two inclusive ends.
If a "user" value is specified in task configuration and that user has a user id in
the given ranges, the task will error before starting. This will not be checked on Windows
clients.
```hcl
config {
denied_host_uids = "0,10-15,22"
}
```
- `denied_host_gids` - (Optional) Specifies a comma-separated list of host gids to
deny. Ranges can be specified by using a hyphen separating the two inclusive ends.
If a "user" value is specified in task configuration and that user is part of
any groups with gid's in the specified ranges, the task will error before
starting. This will not be checked on Windows clients.
```hcl
config {
denied_host_gids = "2,4-8"
}
```
## Client Attributes
The `exec` driver will set the following client attributes:

View File

@@ -130,6 +130,30 @@ client {
- `enabled` - Specifies whether the driver should be enabled or disabled.
Defaults to `false`.
- `denied_host_uids` - (Optional) Specifies a comma-separated list of host uids to
deny. Ranges can be specified by using a hyphen separating the two inclusive ends.
If a "user" value is specified in task configuration and that user has a user id in
the given ranges, the task will error before starting. This will not be checked on Windows
clients.
```hcl
config {
denied_host_uids = "0,10-15,22"
}
```
- `denied_host_gids` - (Optional) Specifies a comma-separated list of host gids to
deny. Ranges can be specified by using a hyphen separating the two inclusive ends.
If a "user" value is specified in task configuration and that user is part of
any groups with gid's in the specified ranges, the task will error before
starting. This will not be checked on Windows clients.
```hcl
config {
denied_host_gids = "2,4-8"
}
```
## Client Options
~> Note: client configuration options will soon be deprecated. Please use
@@ -166,7 +190,6 @@ resources {
}
```
[hardening]: /nomad/docs/install/production/requirements#user-permissions
[plugin-options]: #plugin-options
[plugin-block]: /nomad/docs/configuration/plugin