From ffb83e1ef182e04b8f625112cfe5cbaf1f314e08 Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Fri, 2 Aug 2019 15:20:14 -0400 Subject: [PATCH] client/template: configuration for function blacklist and sandboxing When rendering a task template, the `plugin` function is no longer permitted by default and will raise an error. An operator can opt-in to permitting this function with the new `template.function_blacklist` field in the client configuration. When rendering a task template, path parameters for the `file` function will be treated as relative to the task directory by default. Relative paths or symlinks that point outside the task directory will raise an error. An operator can opt-out of this protection with the new `template.disable_file_sandbox` field in the client configuration. --- .../taskrunner/template/template.go | 14 +++--- .../taskrunner/template/template_test.go | 9 +++- client/config/config.go | 44 ++++++++++++------- command/agent/agent.go | 2 + command/agent/config.go | 29 ++++++++++++ command/agent/config_test.go | 8 ++++ .../source/docs/configuration/client.html.md | 17 +++++++ 7 files changed, 100 insertions(+), 23 deletions(-) diff --git a/client/allocrunner/taskrunner/template/template.go b/client/allocrunner/taskrunner/template/template.go index fe1338c40b9..e22bea0ce4b 100644 --- a/client/allocrunner/taskrunner/template/template.go +++ b/client/allocrunner/taskrunner/template/template.go @@ -545,11 +545,11 @@ func maskProcessEnv(env map[string]string) map[string]string { // parseTemplateConfigs converts the tasks templates in the config into // consul-templates -func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[ctconf.TemplateConfig]*structs.Template, error) { +func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.TemplateConfig]*structs.Template, error) { allowAbs := config.ClientConfig.ReadBoolDefault(hostSrcOption, true) taskEnv := config.EnvBuilder.Build() - ctmpls := make(map[ctconf.TemplateConfig]*structs.Template, len(config.Templates)) + ctmpls := make(map[*ctconf.TemplateConfig]*structs.Template, len(config.Templates)) for _, tmpl := range config.Templates { var src, dest string if tmpl.SourcePath != "" { @@ -573,6 +573,10 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[ctconf.Templat ct.Contents = &tmpl.EmbeddedTmpl ct.LeftDelim = &tmpl.LeftDelim ct.RightDelim = &tmpl.RightDelim + ct.FunctionBlacklist = config.ClientConfig.TemplateConfig.FunctionBlacklist + if !config.ClientConfig.TemplateConfig.DisableSandbox { + ct.SandboxPath = &config.TaskDir + } // Set the permissions if tmpl.Perms != "" { @@ -585,7 +589,7 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[ctconf.Templat } ct.Finalize() - ctmpls[*ct] = tmpl + ctmpls[ct] = tmpl } return ctmpls, nil @@ -594,7 +598,7 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[ctconf.Templat // newRunnerConfig returns a consul-template runner configuration, setting the // Vault and Consul configurations based on the clients configs. func newRunnerConfig(config *TaskTemplateManagerConfig, - templateMapping map[ctconf.TemplateConfig]*structs.Template) (*ctconf.Config, error) { + templateMapping map[*ctconf.TemplateConfig]*structs.Template) (*ctconf.Config, error) { cc := config.ClientConfig conf := ctconf.DefaultConfig() @@ -603,7 +607,7 @@ func newRunnerConfig(config *TaskTemplateManagerConfig, flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(templateMapping))) for ctmpl := range templateMapping { local := ctmpl - flat = append(flat, &local) + flat = append(flat, local) } conf.Templates = &flat diff --git a/client/allocrunner/taskrunner/template/template_test.go b/client/allocrunner/taskrunner/template/template_test.go index bf834f512c5..4a563a850ba 100644 --- a/client/allocrunner/taskrunner/template/template_test.go +++ b/client/allocrunner/taskrunner/template/template_test.go @@ -125,8 +125,13 @@ func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault b mockHooks: NewMockTaskHooks(), templates: templates, node: mock.Node(), - config: &config.Config{Region: region}, - emitRate: DefaultMaxTemplateEventRate, + config: &config.Config{ + Region: region, + TemplateConfig: &config.ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + }}, + emitRate: DefaultMaxTemplateEventRate, } // Build the task environment diff --git a/client/config/config.go b/client/config/config.go index d6e09fde999..f03a1fb05e2 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -201,6 +201,9 @@ type Config struct { // DisableRemoteExec disables remote exec targeting tasks on this client DisableRemoteExec bool + // TemplateConfig includes configuration for template rendering + TemplateConfig *ClientTemplateConfig + // BackwardsCompatibleMetrics determines whether to show methods of // displaying metrics for older versions, or to only show the new format BackwardsCompatibleMetrics bool @@ -239,6 +242,11 @@ type Config struct { HostVolumes map[string]*structs.ClientHostVolumeConfig } +type ClientTemplateConfig struct { + FunctionBlacklist []string + DisableSandbox bool +} + func (c *Config) Copy() *Config { nc := new(Config) *nc = *c @@ -254,22 +262,26 @@ func (c *Config) Copy() *Config { // DefaultConfig returns the default configuration func DefaultConfig() *Config { return &Config{ - Version: version.GetVersion(), - VaultConfig: config.DefaultVaultConfig(), - ConsulConfig: config.DefaultConsulConfig(), - LogOutput: os.Stderr, - Region: "global", - StatsCollectionInterval: 1 * time.Second, - TLSConfig: &config.TLSConfig{}, - LogLevel: "DEBUG", - GCInterval: 1 * time.Minute, - GCParallelDestroys: 2, - GCDiskUsageThreshold: 80, - GCInodeUsageThreshold: 70, - GCMaxAllocs: 50, - NoHostUUID: true, - DisableTaggedMetrics: false, - DisableRemoteExec: false, + Version: version.GetVersion(), + VaultConfig: config.DefaultVaultConfig(), + ConsulConfig: config.DefaultConsulConfig(), + LogOutput: os.Stderr, + Region: "global", + StatsCollectionInterval: 1 * time.Second, + TLSConfig: &config.TLSConfig{}, + LogLevel: "DEBUG", + GCInterval: 1 * time.Minute, + GCParallelDestroys: 2, + GCDiskUsageThreshold: 80, + GCInodeUsageThreshold: 70, + GCMaxAllocs: 50, + NoHostUUID: true, + DisableTaggedMetrics: false, + DisableRemoteExec: false, + TemplateConfig: &ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + }, BackwardsCompatibleMetrics: false, RPCHoldTimeout: 5 * time.Second, } diff --git a/command/agent/agent.go b/command/agent/agent.go index 6cfe7266bad..475d7379b0b 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -468,6 +468,8 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) { conf.ClientMaxPort = uint(agentConfig.Client.ClientMaxPort) conf.ClientMinPort = uint(agentConfig.Client.ClientMinPort) conf.DisableRemoteExec = agentConfig.Client.DisableRemoteExec + conf.TemplateConfig.FunctionBlacklist = agentConfig.Client.TemplateConfig.FunctionBlacklist + conf.TemplateConfig.DisableSandbox = agentConfig.Client.TemplateConfig.DisableSandbox hvMap := make(map[string]*structs.ClientHostVolumeConfig, len(agentConfig.Client.HostVolumes)) for _, v := range agentConfig.Client.HostVolumes { diff --git a/command/agent/config.go b/command/agent/config.go index 435484a7794..5c36e376273 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -242,6 +242,9 @@ type ClientConfig struct { // DisableRemoteExec disables remote exec targeting tasks on this client DisableRemoteExec bool `hcl:"disable_remote_exec"` + // TemplateConfig includes configuration for template rendering + TemplateConfig *ClientTemplateConfig `hcl:"template"` + // ServerJoin contains information that is used to attempt to join servers ServerJoin *ServerJoin `hcl:"server_join"` @@ -266,6 +269,20 @@ type ClientConfig struct { BridgeNetworkSubnet string `hcl:"bridge_network_subnet"` } +// ClientTemplateConfig is configuration on the client specific to template +// rendering +type ClientTemplateConfig struct { + + // FunctionBlacklist disables functions in consul-template that + // are unsafe because they expose information from the client host. + FunctionBlacklist []string `hcl:"function_blacklist"` + + // DisableSandbox allows templates to access arbitrary files on the + // client host. By default templates can access files only within + // the task directory. + DisableSandbox bool `hcl:"disable_file_sandbox"` +} + // ACLConfig is configuration specific to the ACL system type ACLConfig struct { // Enabled controls if we are enforce and manage ACLs @@ -675,6 +692,10 @@ func DevConfig() *Config { conf.Client.GCDiskUsageThreshold = 99 conf.Client.GCInodeUsageThreshold = 99 conf.Client.GCMaxAllocs = 50 + conf.Client.TemplateConfig = &ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + } conf.Telemetry.PrometheusMetrics = true conf.Telemetry.PublishAllocationMetrics = true conf.Telemetry.PublishNodeMetrics = true @@ -716,6 +737,10 @@ func DefaultConfig() *Config { RetryInterval: 30 * time.Second, RetryMaxAttempts: 0, }, + TemplateConfig: &ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + }, }, Server: &ServerConfig{ Enabled: false, @@ -1295,6 +1320,10 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig { result.DisableRemoteExec = b.DisableRemoteExec } + if b.TemplateConfig != nil { + result.TemplateConfig = b.TemplateConfig + } + // Add the servers result.Servers = append(result.Servers, b.Servers...) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index d4fea75110a..d5d49bc4be5 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -94,6 +94,10 @@ func TestConfig_Merge(t *testing.T) { MaxKillTimeout: "20s", ClientMaxPort: 19996, DisableRemoteExec: false, + TemplateConfig: &ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + }, Reserved: &Resources{ CPU: 10, MemoryMB: 10, @@ -253,6 +257,10 @@ func TestConfig_Merge(t *testing.T) { MemoryMB: 105, MaxKillTimeout: "50s", DisableRemoteExec: false, + TemplateConfig: &ClientTemplateConfig{ + FunctionBlacklist: []string{"plugin"}, + DisableSandbox: false, + }, Reserved: &Resources{ CPU: 15, MemoryMB: 15, diff --git a/website/source/docs/configuration/client.html.md b/website/source/docs/configuration/client.html.md index 8e50f53404c..b40af69ac08 100644 --- a/website/source/docs/configuration/client.html.md +++ b/website/source/docs/configuration/client.html.md @@ -147,6 +147,10 @@ driver) but will be removed in a future release. - `bridge_network_subnet` `(string: "172.26.66.0/23")` - Specifies the subnet which the client will use to allocate IP addresses from. +- `template` ([Template](#template-parameters): nil) - Specifies + controls on the behavior of task [`template`](/docs/job-specification/template.html) stanzas. + + ### `chroot_env` Parameters Drivers based on [isolated fork/exec](/docs/drivers/exec.html) implement file @@ -329,6 +333,19 @@ see the [drivers documentation](/docs/drivers/index.html). reserve on all fingerprinted network devices. Ranges can be specified by using a hyphen separated the two inclusive ends. + +### `template` Parameters + +- `function_blacklist` `([]string: ["plugin"])` - Specifies a list of template + rendering functions that should be disallowed in job specs. By default the + `plugin` function is disallowed as it allows running arbitrary commands on + the host as root (unless Nomad is configured to run as a non-root user). + +- `disable_file_sandbox` `(bool: false)` - Allows templates access to arbitrary + files on the client host via the `file` function. By default templates can + access files only within the task directory. + + ## `client` Examples ### Common Setup