mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 17:05:43 +03:00
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:
3
.changelog/20073.txt
Normal file
3
.changelog/20073.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
drivers: add posibility to restrict user and group for exec and rawexec
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
30
drivers/rawexec/driver_unix.go
Normal file
30
drivers/rawexec/driver_unix.go
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
18
drivers/rawexec/driver_windows.go
Normal file
18
drivers/rawexec/driver_windows.go
Normal 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
|
||||
}
|
||||
151
drivers/shared/validators/validators.go
Normal file
151
drivers/shared/validators/validators.go
Normal 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
|
||||
}
|
||||
20
drivers/shared/validators/validators_default.go
Normal file
20
drivers/shared/validators/validators_default.go
Normal 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
|
||||
}
|
||||
122
drivers/shared/validators/validators_test.go
Normal file
122
drivers/shared/validators/validators_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
41
drivers/shared/validators/validators_unix.go
Normal file
41
drivers/shared/validators/validators_unix.go
Normal 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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user