From 19810365f603c552819a433c0e905f2673f3feaa Mon Sep 17 00:00:00 2001 From: Drew Bailey <2614075+drewbailey@users.noreply.github.com> Date: Fri, 17 Jul 2020 10:41:45 -0400 Subject: [PATCH] oss compoments for multi-vault namespaces adds in oss components to support enterprise multi-vault namespace feature upgrade specific doc on vault multi-namespaces vault docs update test to reflect new error --- api/jobs.go | 4 + api/jobs_test.go | 8 ++ api/tasks.go | 4 + .../taskrunner/task_runner_getters.go | 8 +- .../taskrunner/template/template.go | 9 +++ .../taskrunner/template/template_test.go | 35 ++++++++ .../allocrunner/taskrunner/template_hook.go | 10 +++ client/client.go | 1 + command/agent/job_endpoint.go | 40 ++++++---- command/agent/job_endpoint_test.go | 24 +++--- command/job_run.go | 18 ++++- helper/funcs.go | 21 +++++ helper/funcs_test.go | 46 +++++++++++ jobspec/parse.go | 1 + jobspec/parse_test.go | 1 + jobspec/test-fixtures/basic.hcl | 3 +- nomad/job_endpoint.go | 6 ++ nomad/job_endpoint_oss.go | 22 +++++- nomad/job_endpoint_test.go | 51 ++++++++++++ nomad/server.go | 3 +- nomad/server_setup_oss.go | 4 + nomad/structs/diff_test.go | 16 ++++ nomad/structs/funcs.go | 20 +++++ nomad/structs/structs.go | 6 ++ nomad/vault.go | 79 +++++++++++++++---- nomad/vault_test.go | 72 ++++++++--------- vendor/github.com/hashicorp/nomad/api/jobs.go | 4 + .../github.com/hashicorp/nomad/api/tasks.go | 4 + .../pages/docs/job-specification/vault.mdx | 20 +++++ .../pages/docs/upgrade/upgrade-specific.mdx | 10 +++ 30 files changed, 467 insertions(+), 83 deletions(-) diff --git a/api/jobs.go b/api/jobs.go index 8308f0a7fc6..bd79537252f 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -787,6 +787,7 @@ type Job struct { Meta map[string]string ConsulToken *string `mapstructure:"consul_token"` VaultToken *string `mapstructure:"vault_token"` + VaultNamespace *string `mapstructure:"vault_namespace"` NomadTokenID *string `mapstructure:"nomad_token_id"` Status *string StatusDescription *string @@ -850,6 +851,9 @@ func (j *Job) Canonicalize() { if j.VaultToken == nil { j.VaultToken = stringToPtr("") } + if j.VaultNamespace == nil { + j.VaultNamespace = stringToPtr("") + } if j.NomadTokenID == nil { j.NomadTokenID = stringToPtr("") } diff --git a/api/jobs_test.go b/api/jobs_test.go index 150ecfc0831..4eb62e4e519 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -243,6 +243,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Status: stringToPtr(""), StatusDescription: stringToPtr(""), @@ -333,6 +334,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Status: stringToPtr(""), StatusDescription: stringToPtr(""), @@ -406,6 +408,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), @@ -572,6 +575,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), @@ -730,6 +734,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), @@ -816,6 +821,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), @@ -981,6 +987,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), @@ -1144,6 +1151,7 @@ func TestJobs_Canonicalize(t *testing.T) { AllAtOnce: boolToPtr(false), ConsulToken: stringToPtr(""), VaultToken: stringToPtr(""), + VaultNamespace: stringToPtr(""), NomadTokenID: stringToPtr(""), Stop: boolToPtr(false), Stable: boolToPtr(false), diff --git a/api/tasks.go b/api/tasks.go index b9b79af5470..8275813e6ee 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -812,6 +812,7 @@ func (tmpl *Template) Canonicalize() { type Vault struct { Policies []string + Namespace *string `mapstructure:"namespace"` Env *bool ChangeMode *string `mapstructure:"change_mode"` ChangeSignal *string `mapstructure:"change_signal"` @@ -821,6 +822,9 @@ func (v *Vault) Canonicalize() { if v.Env == nil { v.Env = boolToPtr(true) } + if v.Namespace == nil { + v.Namespace = stringToPtr("") + } if v.ChangeMode == nil { v.ChangeMode = stringToPtr("restart") } diff --git a/client/allocrunner/taskrunner/task_runner_getters.go b/client/allocrunner/taskrunner/task_runner_getters.go index d962fcab5b6..5801297c386 100644 --- a/client/allocrunner/taskrunner/task_runner_getters.go +++ b/client/allocrunner/taskrunner/task_runner_getters.go @@ -57,7 +57,13 @@ func (tr *TaskRunner) setVaultToken(token string) { tr.vaultToken = token // Update the task's environment - tr.envBuilder.SetVaultToken(token, tr.clientConfig.VaultConfig.Namespace, tr.task.Vault.Env) + taskNamespace := tr.task.Vault.Namespace + + ns := tr.clientConfig.VaultConfig.Namespace + if taskNamespace != "" { + ns = taskNamespace + } + tr.envBuilder.SetVaultToken(token, ns, tr.task.Vault.Env) } // getDriverHandle returns a driver handle. diff --git a/client/allocrunner/taskrunner/template/template.go b/client/allocrunner/taskrunner/template/template.go index a0e7c2bbd6b..caf3f99bcbf 100644 --- a/client/allocrunner/taskrunner/template/template.go +++ b/client/allocrunner/taskrunner/template/template.go @@ -87,6 +87,9 @@ type TaskTemplateManagerConfig struct { // VaultToken is the Vault token for the task. VaultToken string + // VaultNamespace is the Vault namespace for the task + VaultNamespace string + // TaskDir is the task's directory TaskDir string @@ -655,9 +658,15 @@ func newRunnerConfig(config *TaskTemplateManagerConfig, if cc.VaultConfig != nil && cc.VaultConfig.IsEnabled() { conf.Vault.Address = &cc.VaultConfig.Addr conf.Vault.Token = &config.VaultToken + + // Set the Vault Namespace. Passed in Task config has + // highest precedence. if config.ClientConfig.VaultConfig.Namespace != "" { conf.Vault.Namespace = &config.ClientConfig.VaultConfig.Namespace } + if config.VaultNamespace != "" { + conf.Vault.Namespace = &config.VaultNamespace + } if strings.HasPrefix(cc.VaultConfig.Addr, "https") || cc.VaultConfig.TLSCertFile != "" { skipVerify := cc.VaultConfig.TLSSkipVerify != nil && *cc.VaultConfig.TLSSkipVerify diff --git a/client/allocrunner/taskrunner/template/template_test.go b/client/allocrunner/taskrunner/template/template_test.go index 3b51af3f4b3..459149ca4fd 100644 --- a/client/allocrunner/taskrunner/template/template_test.go +++ b/client/allocrunner/taskrunner/template/template_test.go @@ -1377,6 +1377,41 @@ func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) { assert.Equal(testNS, *ctconf.Vault.Namespace, "Vault Namespace Value") } +// TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is +// propagated to consul-template's configuration. +func TestTaskTemplateManager_Config_VaultNamespace_TaskOverride(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testNS := "test-namespace" + c := config.DefaultConfig() + c.Node = mock.Node() + c.VaultConfig = &sconfig.VaultConfig{ + Enabled: helper.BoolToPtr(true), + Addr: "https://localhost/", + TLSServerName: "notlocalhost", + Namespace: testNS, + } + + alloc := mock.Alloc() + overriddenNS := "new-namespace" + + // Set the template manager config vault namespace + config := &TaskTemplateManagerConfig{ + ClientConfig: c, + VaultToken: "token", + VaultNamespace: overriddenNS, + EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region), + } + + ctmplMapping, err := parseTemplateConfigs(config) + assert.Nil(err, "Parsing Templates") + + ctconf, err := newRunnerConfig(config, ctmplMapping) + assert.Nil(err, "Building Runner Config") + assert.Equal(overriddenNS, *ctconf.Vault.Namespace, "Vault Namespace Value") +} + func TestTaskTemplateManager_BlockedEvents(t *testing.T) { // The tests sets a template that need keys 0, 1, 2, 3, 4, // then subsequently sets 0, 1, 2 keys diff --git a/client/allocrunner/taskrunner/template_hook.go b/client/allocrunner/taskrunner/template_hook.go index 1c76d144792..58150bbabb7 100644 --- a/client/allocrunner/taskrunner/template_hook.go +++ b/client/allocrunner/taskrunner/template_hook.go @@ -47,6 +47,9 @@ type templateHook struct { // vaultToken is the current Vault token vaultToken string + // vaultNamespace is the current Vault namespace + vaultNamespace string + // taskDir is the task directory taskDir string } @@ -75,6 +78,12 @@ func (h *templateHook) Prestart(ctx context.Context, req *interfaces.TaskPrestar // Store the current Vault token and the task directory h.taskDir = req.TaskDir.Dir h.vaultToken = req.VaultToken + + // Set vault namespace if specified + if req.Task.Vault != nil { + h.vaultNamespace = req.Task.Vault.Namespace + } + unblockCh, err := h.newManager() if err != nil { return err @@ -98,6 +107,7 @@ func (h *templateHook) newManager() (unblock chan struct{}, err error) { Templates: h.config.templates, ClientConfig: h.config.clientConfig, VaultToken: h.vaultToken, + VaultNamespace: h.vaultNamespace, TaskDir: h.taskDir, EnvBuilder: h.config.envBuilder, MaxTemplateEventRate: template.DefaultMaxTemplateEventRate, diff --git a/client/client.go b/client/client.go index 3cac9ddcde5..31a62c8ad68 100644 --- a/client/client.go +++ b/client/client.go @@ -2457,6 +2457,7 @@ func (c *Client) deriveToken(alloc *structs.Allocation, taskNames []string, vcli } // Derive the tokens + // namespace is handled via nomad/vault var resp structs.DeriveVaultTokenResponse if err := c.RPC("Node.DeriveVaultToken", &req, &resp); err != nil { vlogger.Error("error making derive token RPC", "error", err) diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index f112372f609..a081f44529c 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -773,22 +773,23 @@ func ApiJobToStructJob(job *api.Job) *structs.Job { job.Canonicalize() j := &structs.Job{ - Stop: *job.Stop, - Region: *job.Region, - Namespace: *job.Namespace, - ID: *job.ID, - ParentID: *job.ParentID, - Name: *job.Name, - Type: *job.Type, - Priority: *job.Priority, - AllAtOnce: *job.AllAtOnce, - Datacenters: job.Datacenters, - Payload: job.Payload, - Meta: job.Meta, - ConsulToken: *job.ConsulToken, - VaultToken: *job.VaultToken, - Constraints: ApiConstraintsToStructs(job.Constraints), - Affinities: ApiAffinitiesToStructs(job.Affinities), + Stop: *job.Stop, + Region: *job.Region, + Namespace: *job.Namespace, + ID: *job.ID, + ParentID: *job.ParentID, + Name: *job.Name, + Type: *job.Type, + Priority: *job.Priority, + AllAtOnce: *job.AllAtOnce, + Datacenters: job.Datacenters, + Payload: job.Payload, + Meta: job.Meta, + ConsulToken: *job.ConsulToken, + VaultToken: *job.VaultToken, + VaultNamespace: *job.VaultNamespace, + Constraints: ApiConstraintsToStructs(job.Constraints), + Affinities: ApiAffinitiesToStructs(job.Affinities), } // Update has been pushed into the task groups. stagger and max_parallel are @@ -976,6 +977,12 @@ func ApiTgToStructsTG(job *structs.Job, taskGroup *api.TaskGroup, tg *structs.Ta for l, task := range taskGroup.Tasks { t := &structs.Task{} ApiTaskToStructsTask(task, t) + + // Set the tasks vault namespace from Job if it was not + // specified by the task or group + if t.Vault != nil && t.Vault.Namespace == "" && job.VaultNamespace != "" { + t.Vault.Namespace = job.VaultNamespace + } tg.Tasks[l] = t } } @@ -1089,6 +1096,7 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) { if apiTask.Vault != nil { structsTask.Vault = &structs.Vault{ Policies: apiTask.Vault.Policies, + Namespace: *apiTask.Vault.Namespace, Env: *apiTask.Vault.Env, ChangeMode: *apiTask.Vault.ChangeMode, ChangeSignal: *apiTask.Vault.ChangeSignal, diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index cb3fbfb54d0..8549ccbc30c 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -2121,6 +2121,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { }, }, Vault: &api.Vault{ + Namespace: helper.StringToPtr("ns1"), Policies: []string{"a", "b", "c"}, Env: helper.BoolToPtr(true), ChangeMode: helper.StringToPtr("c"), @@ -2149,6 +2150,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { }, ConsulToken: helper.StringToPtr("abc123"), VaultToken: helper.StringToPtr("def456"), + VaultNamespace: helper.StringToPtr("ghi789"), Status: helper.StringToPtr("status"), StatusDescription: helper.StringToPtr("status_desc"), Version: helper.Uint64ToPtr(10), @@ -2158,16 +2160,17 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { } expected := &structs.Job{ - Stop: true, - Region: "global", - Namespace: "foo", - ID: "foo", - ParentID: "lol", - Name: "name", - Type: "service", - Priority: 50, - AllAtOnce: true, - Datacenters: []string{"dc1", "dc2"}, + Stop: true, + Region: "global", + Namespace: "foo", + VaultNamespace: "ghi789", + ID: "foo", + ParentID: "lol", + Name: "name", + Type: "service", + Priority: 50, + AllAtOnce: true, + Datacenters: []string{"dc1", "dc2"}, Constraints: []*structs.Constraint{ { LTarget: "a", @@ -2488,6 +2491,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { }, }, Vault: &structs.Vault{ + Namespace: "ns1", Policies: []string{"a", "b", "c"}, Env: true, ChangeMode: "c", diff --git a/command/job_run.go b/command/job_run.go index 3af389b2183..3ad750c5370 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -101,6 +101,11 @@ Run Options: the job file. This overrides the token found in $VAULT_TOKEN environment variable and that found in the job. + -vault-namespace + If set, the passed Vault namespace is stored in the job before sending to the + Nomad servers. This overrides the namespace found in $VAULT_NAMESPACE environment + variable and that found in the job. + -verbose Display full information. ` @@ -119,6 +124,7 @@ func (c *JobRunCommand) AutocompleteFlags() complete.Flags { "-verbose": complete.PredictNothing, "-consul-token": complete.PredictNothing, "-vault-token": complete.PredictAnything, + "-vault-namespace": complete.PredictAnything, "-output": complete.PredictNothing, "-policy-override": complete.PredictNothing, "-preserve-counts": complete.PredictNothing, @@ -133,7 +139,7 @@ func (c *JobRunCommand) Name() string { return "job run" } func (c *JobRunCommand) Run(args []string) int { var detach, verbose, output, override, preserveCounts bool - var checkIndexStr, consulToken, vaultToken string + var checkIndexStr, consulToken, vaultToken, vaultNamespace string flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } @@ -145,6 +151,7 @@ func (c *JobRunCommand) Run(args []string) int { flags.StringVar(&checkIndexStr, "check-index", "", "") flags.StringVar(&consulToken, "consul-token", "", "") flags.StringVar(&vaultToken, "vault-token", "", "") + flags.StringVar(&vaultNamespace, "vault-namespace", "", "") if err := flags.Parse(args); err != nil { return 1 @@ -213,6 +220,15 @@ func (c *JobRunCommand) Run(args []string) int { job.VaultToken = helper.StringToPtr(vaultToken) } + // Parse the Vault namespace + if vaultNamespace == "" { + vaultNamespace = os.Getenv("VAULT_NAMESPACE") + } + + if vaultNamespace != "" { + job.VaultNamespace = helper.StringToPtr(vaultNamespace) + } + if output { req := struct { Job *api.Job diff --git a/helper/funcs.go b/helper/funcs.go index c75294a1b69..e6390720cbe 100644 --- a/helper/funcs.go +++ b/helper/funcs.go @@ -3,6 +3,7 @@ package helper import ( "crypto/sha512" "fmt" + "path/filepath" "reflect" "regexp" "strings" @@ -461,3 +462,23 @@ func RemoveEqualFold(xs *[]string, search string) { } } } + +// CheckNamespaceScope ensures that the provided namespace is equal to +// or a parent of the requested namespaces. Returns requested namespaces +// which are not equal to or a child of the provided namespace. +func CheckNamespaceScope(provided string, requested []string) []string { + var offending []string + for _, ns := range requested { + rel, err := filepath.Rel(provided, ns) + if err != nil { + offending = append(offending, ns) + // If relative path requires ".." it's not a child + } else if strings.Contains(rel, "..") { + offending = append(offending, ns) + } + } + if len(offending) > 0 { + return offending + } + return nil +} diff --git a/helper/funcs_test.go b/helper/funcs_test.go index cff3e9c21cf..ea6f91bdd23 100644 --- a/helper/funcs_test.go +++ b/helper/funcs_test.go @@ -5,6 +5,8 @@ import ( "reflect" "sort" "testing" + + "github.com/stretchr/testify/require" ) func TestSliceStringIsSubset(t *testing.T) { @@ -157,3 +159,47 @@ func BenchmarkCleanEnvVar(b *testing.B) { CleanEnvVar(in, replacement) } } + +func TestCheckNamespaceScope(t *testing.T) { + cases := []struct { + desc string + provided string + requested []string + offending []string + }{ + { + desc: "root ns requesting namespace", + provided: "", + requested: []string{"engineering"}, + }, + { + desc: "matching parent ns with child", + provided: "engineering", + requested: []string{"engineering", "engineering/sub-team"}, + }, + { + desc: "mismatch ns", + provided: "engineering", + requested: []string{"finance", "engineering/sub-team", "eng"}, + offending: []string{"finance", "eng"}, + }, + { + desc: "mismatch child", + provided: "engineering/sub-team", + requested: []string{"engineering/new-team", "engineering/sub-team", "engineering/sub-team/child"}, + offending: []string{"engineering/new-team"}, + }, + { + desc: "matching prefix", + provided: "engineering", + requested: []string{"engineering/new-team", "engineering/new-team/sub-team"}, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + offending := CheckNamespaceScope(tc.provided, tc.requested) + require.Equal(t, offending, tc.offending) + }) + } +} diff --git a/jobspec/parse.go b/jobspec/parse.go index 7d386abd8a6..f6e7b8584c8 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -506,6 +506,7 @@ func parseVault(result *api.Vault, list *ast.ObjectList) error { // Check for invalid keys valid := []string{ + "namespace", "policies", "env", "change_mode", diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 73491645060..dfc64638694 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -332,6 +332,7 @@ func TestParse(t *testing.T) { }, }, Vault: &api.Vault{ + Namespace: helper.StringToPtr("ns1"), Policies: []string{"foo", "bar"}, Env: helper.BoolToPtr(true), ChangeMode: helper.StringToPtr(structs.VaultChangeModeRestart), diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl index 8b2f9ef74d6..1b99ee725ea 100644 --- a/jobspec/test-fixtures/basic.hcl +++ b/jobspec/test-fixtures/basic.hcl @@ -292,7 +292,8 @@ job "binstore-storagelocker" { } vault { - policies = ["foo", "bar"] + namespace = "ns1" + policies = ["foo", "bar"] } template { diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index 1980dbd01da..65b32d95d5b 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -232,6 +232,12 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis return err } + // Check Namespaces + namespaceErr := j.multiVaultNamespaceValidation(policies, s) + if namespaceErr != nil { + return namespaceErr + } + // If we are given a root token it can access all policies if !lib.StrContains(allowedPolicies, "root") { flatPolicies := structs.VaultPoliciesSet(policies) diff --git a/nomad/job_endpoint_oss.go b/nomad/job_endpoint_oss.go index 08755c47a3d..b7ad70b1ba7 100644 --- a/nomad/job_endpoint_oss.go +++ b/nomad/job_endpoint_oss.go @@ -2,7 +2,13 @@ package nomad -import "github.com/hashicorp/nomad/nomad/structs" +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/nomad/structs" + vapi "github.com/hashicorp/vault/api" +) // enforceSubmitJob is used to check any Sentinel policies for the submit-job scope func (j *Job) enforceSubmitJob(override bool, job *structs.Job) (error, error) { @@ -23,3 +29,17 @@ func (j *Job) multiregionStart(args *structs.JobRegisterRequest, reply *structs. func (j *Job) interpolateMultiregionFields(args *structs.JobPlanRequest) error { return nil } + +// multiVaultNamespaceValidation provides a convience check to ensure +// multiple vault namespaces were not requested, this returns an early friendly +// error before job registry and further feature checks. +func (j *Job) multiVaultNamespaceValidation( + policies map[string]map[string]*structs.Vault, + s *vapi.Secret, +) error { + requestedNamespaces := structs.VaultNamespaceSet(policies) + if len(requestedNamespaces) > 0 { + return fmt.Errorf("multiple vault namespaces requires Nomad Enterprise, Namespaces: %s", strings.Join(requestedNamespaces, ", ")) + } + return nil +} diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 9edd97c279c..4be53e0338b 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -1490,6 +1490,57 @@ func TestJobEndpoint_Register_Vault_Policies(t *testing.T) { } } +func TestJobEndpoint_Register_Vault_MultiNamespaces(t *testing.T) { + t.Parallel() + + s1, cleanupS1 := TestServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + defer cleanupS1() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + // Enable vault + tr, f := true, false + s1.config.VaultConfig.Enabled = &tr + s1.config.VaultConfig.AllowUnauthenticated = &f + + // Replace the Vault Client on the server + tvc := &TestVaultClient{} + s1.vault = tvc + + goodToken := uuid.Generate() + goodPolicies := []string{"foo", "bar", "baz"} + tvc.SetLookupTokenAllowedPolicies(goodToken, goodPolicies) + + // Create the register request with a job asking for a vault policy but + // don't send a Vault token + job := mock.Job() + job.VaultToken = goodToken + job.TaskGroups[0].Tasks[0].Vault = &structs.Vault{ + Namespace: "ns1", + Policies: []string{"foo"}, + ChangeMode: structs.VaultChangeModeRestart, + } + req := &structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{ + Region: "global", + Namespace: job.Namespace, + }, + } + + // Fetch the response + var resp structs.JobRegisterResponse + err := msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp) + // OSS or Ent check + if s1.EnterpriseState.Features() == 0 { + require.Contains(t, err.Error(), "multiple vault namespaces requires Nomad Enterprise") + } else { + require.NoError(t, err) + } +} + // TestJobEndpoint_Register_SemverConstraint asserts that semver ordering is // used when evaluating semver constraints. func TestJobEndpoint_Register_SemverConstraint(t *testing.T) { diff --git a/nomad/server.go b/nomad/server.go index fd378021df4..ed36d6074fc 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -1061,7 +1061,8 @@ func (s *Server) setupConsul(consulACLs consul.ACLsAPI) { // setupVaultClient is used to set up the Vault API client. func (s *Server) setupVaultClient() error { - v, err := NewVaultClient(s.config.VaultConfig, s.logger, s.purgeVaultAccessors) + delegate := s.entVaultDelegate() + v, err := NewVaultClient(s.config.VaultConfig, s.logger, s.purgeVaultAccessors, delegate) if err != nil { return err } diff --git a/nomad/server_setup_oss.go b/nomad/server_setup_oss.go index 267638d01b8..f5ca226cbda 100644 --- a/nomad/server_setup_oss.go +++ b/nomad/server_setup_oss.go @@ -24,3 +24,7 @@ func (s *Server) setupEnterprise(config *Config) error { return nil } func (s *Server) startEnterpriseBackground() {} + +func (s *Server) entVaultDelegate() *VaultNoopDelegate { + return &VaultNoopDelegate{} +} diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go index 9fc774d6bb9..900f80c9b15 100644 --- a/nomad/structs/diff_test.go +++ b/nomad/structs/diff_test.go @@ -5855,6 +5855,7 @@ func TestTaskDiff(t *testing.T) { Name: "Vault edited", Old: &Task{ Vault: &Vault{ + Namespace: "ns1", Policies: []string{"foo", "bar"}, Env: true, ChangeMode: "signal", @@ -5863,6 +5864,7 @@ func TestTaskDiff(t *testing.T) { }, New: &Task{ Vault: &Vault{ + Namespace: "ns2", Policies: []string{"bar", "baz"}, Env: false, ChangeMode: "restart", @@ -5894,6 +5896,12 @@ func TestTaskDiff(t *testing.T) { Old: "true", New: "false", }, + { + Type: DiffTypeEdited, + Name: "Namespace", + Old: "ns1", + New: "ns2", + }, }, Objects: []*ObjectDiff{ { @@ -5924,6 +5932,7 @@ func TestTaskDiff(t *testing.T) { Contextual: true, Old: &Task{ Vault: &Vault{ + Namespace: "ns1", Policies: []string{"foo", "bar"}, Env: true, ChangeMode: "signal", @@ -5932,6 +5941,7 @@ func TestTaskDiff(t *testing.T) { }, New: &Task{ Vault: &Vault{ + Namespace: "ns1", Policies: []string{"bar", "baz"}, Env: true, ChangeMode: "signal", @@ -5963,6 +5973,12 @@ func TestTaskDiff(t *testing.T) { Old: "true", New: "true", }, + { + Type: DiffTypeNone, + Name: "Namespace", + Old: "ns1", + New: "ns1", + }, }, Objects: []*ObjectDiff{ { diff --git a/nomad/structs/funcs.go b/nomad/structs/funcs.go index 855ab7ff0e1..f5d9bdd60d3 100644 --- a/nomad/structs/funcs.go +++ b/nomad/structs/funcs.go @@ -323,6 +323,26 @@ func VaultPoliciesSet(policies map[string]map[string]*Vault) []string { return flattened } +// VaultNaVaultNamespaceSet takes the structure returned by VaultPolicies and +// returns a set of required namespaces +func VaultNamespaceSet(policies map[string]map[string]*Vault) []string { + set := make(map[string]struct{}) + + for _, tgp := range policies { + for _, tp := range tgp { + if tp.Namespace != "" { + set[tp.Namespace] = struct{}{} + } + } + } + + flattened := make([]string, 0, len(set)) + for p := range set { + flattened = append(flattened, p) + } + return flattened +} + // DenormalizeAllocationJobs is used to attach a job to all allocations that are // non-terminal and do not have a job already. This is useful in cases where the // job is normalized. diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 61ad06e325b..7ad54958c5d 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -3804,6 +3804,9 @@ type Job struct { // transfer the token and is not stored after Job submission. VaultToken string + // VaultNamespace is the Vault namepace + VaultNamespace string + // NomadTokenID is the Accessor ID of the ACL token (if any) // used to register this version of the job. Used by deploymentwatcher. NomadTokenID string @@ -7973,6 +7976,9 @@ type Vault struct { // Policies is the set of policies that the task needs access to Policies []string + // Namespace is the vault namespace that should be used. + Namespace string + // Env marks whether the Vault Token should be exposed as an environment // variable Env bool diff --git a/nomad/vault.go b/nomad/vault.go index 013f38be921..6f23a47f5b0 100644 --- a/nomad/vault.go +++ b/nomad/vault.go @@ -160,12 +160,13 @@ type PurgeVaultAccessorFn func(accessors []*structs.VaultAccessor) error // tokenData holds the relevant information about the Vault token passed to the // client. type tokenData struct { - CreationTTL int `mapstructure:"creation_ttl"` - TTL int `mapstructure:"ttl"` - Renewable bool `mapstructure:"renewable"` - Policies []string `mapstructure:"policies"` - Role string `mapstructure:"role"` - Root bool + CreationTTL int `mapstructure:"creation_ttl"` + TTL int `mapstructure:"ttl"` + Renewable bool `mapstructure:"renewable"` + Policies []string `mapstructure:"policies"` + Role string `mapstructure:"role"` + NamespacePath string `mapstructure:"namespace_path"` + Root bool } // vaultClient is the Servers implementation of the VaultClient interface. The @@ -240,11 +241,17 @@ type vaultClient struct { // setConfigLock serializes access to the SetConfig method setConfigLock sync.Mutex + + entHandler taskClientHandler +} + +type taskClientHandler interface { + clientForTask(v *vaultClient, namespace string) (*vapi.Client, error) } // NewVaultClient returns a Vault client from the given config. If the client // couldn't be made an error is returned. -func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVaultAccessorFn) (*vaultClient, error) { +func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVaultAccessorFn, delegate taskClientHandler) (*vaultClient, error) { if c == nil { return nil, fmt.Errorf("must pass valid VaultConfig") } @@ -255,14 +262,18 @@ func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVault if purgeFn == nil { purgeFn = func(accessors []*structs.VaultAccessor) error { return nil } } + if delegate == nil { + delegate = &VaultNoopDelegate{} + } v := &vaultClient{ - config: c, - logger: logger.Named("vault"), - limiter: rate.NewLimiter(requestRateLimit, int(requestRateLimit)), - revoking: make(map[*structs.VaultAccessor]time.Time), - purgeFn: purgeFn, - tomb: &tomb.Tomb{}, + config: c, + logger: logger.Named("vault"), + limiter: rate.NewLimiter(requestRateLimit, int(requestRateLimit)), + revoking: make(map[*structs.VaultAccessor]time.Time), + purgeFn: purgeFn, + tomb: &tomb.Tomb{}, + entHandler: delegate, } if v.config.IsEnabled() { @@ -945,7 +956,6 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta if !v.Active() { return nil, structs.NewRecoverableError(fmt.Errorf("Vault client not active"), true) } - // Check if we have established a connection with Vault if established, err := v.ConnectionEstablished(); !established && err == nil { return nil, structs.NewRecoverableError(fmt.Errorf("Connection to Vault has not been established"), true) @@ -970,6 +980,12 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta return nil, fmt.Errorf("Task does not require Vault policies") } + // Set namespace for task + namespaceForTask := v.config.Namespace + if taskVault.Namespace != "" { + namespaceForTask = taskVault.Namespace + } + // Build the creation request req := &vapi.TokenCreateRequest{ Policies: taskVault.Policies, @@ -977,6 +993,7 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta "AllocationID": a.ID, "Task": task, "NodeID": a.NodeID, + "Namespace": namespaceForTask, }, TTL: v.childTTL, DisplayName: fmt.Sprintf("%s-%s", a.ID, task), @@ -992,12 +1009,19 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta var secret *vapi.Secret var err error role := v.getRole() + + // Fetch client for task + taskClient, err := v.entHandler.clientForTask(v, namespaceForTask) + if err != nil { + return nil, err + } + if v.tokenData.Root && role == "" { req.Period = v.childTTL - secret, err = v.auth.Create(req) + secret, err = taskClient.Auth().Token().Create(req) } else { // Make the token using the role - secret, err = v.auth.CreateWithRole(req, v.getRole()) + secret, err = taskClient.Auth().Token().CreateWithRole(req, v.getRole()) } // Determine whether it is unrecoverable @@ -1064,6 +1088,22 @@ func PoliciesFrom(s *vapi.Secret) ([]string, error) { return s.TokenPolicies() } +// PolicyDataFrom parses the Data returned by a token lookup. +// It should not be used to parse TokenPolicies as the list will not be +// exhaustive. +func PolicyDataFrom(s *vapi.Secret) (tokenData, error) { + if s == nil { + return tokenData{}, fmt.Errorf("cannot parse nil Vault secret") + } + var data tokenData + + if err := mapstructure.WeakDecode(s.Data, &data); err != nil { + return tokenData{}, fmt.Errorf("failed to parse Vault token's data block: %v", err) + } + + return data, nil +} + // RevokeTokens revokes the passed set of accessors. If committed is set, the // purge function passed to the client is called. If there is an error purging // either because of Vault failures or because of the purge function, the @@ -1385,3 +1425,10 @@ func (v *vaultClient) extendExpiration(ttlSeconds int) { v.currentExpiration = time.Now().Add(time.Duration(ttlSeconds) * time.Second) v.currentExpirationLock.Unlock() } + +// VaultVaultNoopDelegate returns the default vault api auth token handler +type VaultNoopDelegate struct{} + +func (e *VaultNoopDelegate) clientForTask(v *vaultClient, namespace string) (*vapi.Client, error) { + return v.client, nil +} diff --git a/nomad/vault_test.go b/nomad/vault_test.go index 3f94d79446e..cd7f3d13d73 100644 --- a/nomad/vault_test.go +++ b/nomad/vault_test.go @@ -157,20 +157,20 @@ func TestVaultClient_BadConfig(t *testing.T) { logger := testlog.HCLogger(t) // Should be no error since Vault is not enabled - _, err := NewVaultClient(nil, logger, nil) + _, err := NewVaultClient(nil, logger, nil, nil) if err == nil || !strings.Contains(err.Error(), "valid") { t.Fatalf("expected config error: %v", err) } tr := true conf.Enabled = &tr - _, err = NewVaultClient(conf, logger, nil) + _, err = NewVaultClient(conf, logger, nil, nil) if err == nil || !strings.Contains(err.Error(), "token must be set") { t.Fatalf("Expected token unset error: %v", err) } conf.Token = "123" - _, err = NewVaultClient(conf, logger, nil) + _, err = NewVaultClient(conf, logger, nil, nil) if err == nil || !strings.Contains(err.Error(), "address must be set") { t.Fatalf("Expected address unset error: %v", err) } @@ -192,7 +192,7 @@ func TestVaultClient_WithNamespaceSupport(t *testing.T) { logger := testlog.HCLogger(t) // Should be no error since Vault is not enabled - c, err := NewVaultClient(conf, logger, nil) + c, err := NewVaultClient(conf, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -217,7 +217,7 @@ func TestVaultClient_WithoutNamespaceSupport(t *testing.T) { logger := testlog.HCLogger(t) // Should be no error since Vault is not enabled - c, err := NewVaultClient(conf, logger, nil) + c, err := NewVaultClient(conf, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -236,7 +236,7 @@ func TestVaultClient_EstablishConnection(t *testing.T) { v := testutil.NewTestVaultDelayed(t) logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -304,7 +304,7 @@ func TestVaultClient_ValidateRole(t *testing.T) { logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) require.NoError(t, err) defer client.Stop() @@ -353,7 +353,7 @@ func TestVaultClient_ValidateRole_Success(t *testing.T) { logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) require.NoError(t, err) defer client.Stop() @@ -399,7 +399,7 @@ func TestVaultClient_ValidateRole_Deprecated_Success(t *testing.T) { logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) require.NoError(t, err) defer client.Stop() @@ -433,7 +433,7 @@ func TestVaultClient_ValidateRole_NonExistent(t *testing.T) { logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond v.Config.Role = "test-nonexistent" - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -481,7 +481,7 @@ func TestVaultClient_ValidateToken(t *testing.T) { logger := testlog.HCLogger(t) v.Config.ConnectionRetryIntv = 100 * time.Millisecond - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -523,7 +523,7 @@ func TestVaultClient_SetActive(t *testing.T) { defer v.Stop() logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -559,7 +559,7 @@ func TestVaultClient_SetConfig(t *testing.T) { v2.Config.Token = defaultTestVaultWhitelistRoleAndToken(v2, t, 20) logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -622,7 +622,7 @@ func TestVaultClient_SetConfig_Deadlock(t *testing.T) { v2.Config.Token = defaultTestVaultWhitelistRoleAndToken(v2, t, 20) logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -647,7 +647,7 @@ func TestVaultClient_SetConfig_Disable(t *testing.T) { defer v.Stop() logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -685,7 +685,7 @@ func TestVaultClient_RenewalLoop(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -721,7 +721,7 @@ func TestVaultClientRenewUpdatesExpiration(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -760,7 +760,7 @@ func TestVaultClient_StopsAfterPermissionError(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -794,7 +794,7 @@ func TestVaultClient_LoopsUntilCannotRenew(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -856,7 +856,7 @@ func TestVaultClient_LookupToken_Invalid(t *testing.T) { // Enable vault but use a bad address so it never establishes a conn logger := testlog.HCLogger(t) - client, err := NewVaultClient(conf, logger, nil) + client, err := NewVaultClient(conf, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -875,7 +875,7 @@ func TestVaultClient_LookupToken_Root(t *testing.T) { defer v.Stop() logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -940,7 +940,7 @@ func TestVaultClient_LookupToken_Role(t *testing.T) { v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5) logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1002,7 +1002,7 @@ func TestVaultClient_LookupToken_RateLimit(t *testing.T) { defer v.Stop() logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1062,7 +1062,7 @@ func TestVaultClient_CreateToken_Root(t *testing.T) { defer v.Stop() logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1110,7 +1110,7 @@ func TestVaultClient_CreateToken_Whitelist_Role(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1161,7 +1161,7 @@ func TestVaultClient_CreateToken_Root_Target_Role(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1220,7 +1220,7 @@ func TestVaultClient_CreateToken_Blacklist_Role(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1269,7 +1269,7 @@ func TestVaultClient_CreateToken_Role_InvalidToken(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1307,7 +1307,7 @@ func TestVaultClient_CreateToken_Role_Unrecoverable(t *testing.T) { // Start the client logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, nil) + client, err := NewVaultClient(v.Config, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1341,7 +1341,7 @@ func TestVaultClient_CreateToken_Prestart(t *testing.T) { } logger := testlog.HCLogger(t) - client, err := NewVaultClient(vconfig, logger, nil) + client, err := NewVaultClient(vconfig, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1372,7 +1372,7 @@ func TestVaultClient_MarkForRevocation(t *testing.T) { Addr: "http://127.0.0.1:0", } logger := testlog.HCLogger(t) - client, err := NewVaultClient(vconfig, logger, nil) + client, err := NewVaultClient(vconfig, logger, nil, nil) require.NoError(t, err) client.SetActive(true) @@ -1400,7 +1400,7 @@ func TestVaultClient_RevokeTokens_PreEstablishs(t *testing.T) { Addr: "http://127.0.0.1:0", } logger := testlog.HCLogger(t) - client, err := NewVaultClient(vconfig, logger, nil) + client, err := NewVaultClient(vconfig, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1446,7 +1446,7 @@ func TestVaultClient_RevokeTokens_Failures_TTL(t *testing.T) { Addr: "http://127.0.0.1:0", } logger := testlog.HCLogger(t) - client, err := NewVaultClient(vconfig, logger, nil) + client, err := NewVaultClient(vconfig, logger, nil, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1494,7 +1494,7 @@ func TestVaultClient_RevokeTokens_Root(t *testing.T) { } logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, purge) + client, err := NewVaultClient(v.Config, logger, purge, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1562,7 +1562,7 @@ func TestVaultClient_RevokeTokens_Role(t *testing.T) { } logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, purge) + client, err := NewVaultClient(v.Config, logger, purge, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } @@ -1633,7 +1633,7 @@ func TestVaultClient_RevokeTokens_Idempotent(t *testing.T) { } logger := testlog.HCLogger(t) - client, err := NewVaultClient(v.Config, logger, purge) + client, err := NewVaultClient(v.Config, logger, purge, nil) if err != nil { t.Fatalf("failed to build vault client: %v", err) } diff --git a/vendor/github.com/hashicorp/nomad/api/jobs.go b/vendor/github.com/hashicorp/nomad/api/jobs.go index 8308f0a7fc6..bd79537252f 100644 --- a/vendor/github.com/hashicorp/nomad/api/jobs.go +++ b/vendor/github.com/hashicorp/nomad/api/jobs.go @@ -787,6 +787,7 @@ type Job struct { Meta map[string]string ConsulToken *string `mapstructure:"consul_token"` VaultToken *string `mapstructure:"vault_token"` + VaultNamespace *string `mapstructure:"vault_namespace"` NomadTokenID *string `mapstructure:"nomad_token_id"` Status *string StatusDescription *string @@ -850,6 +851,9 @@ func (j *Job) Canonicalize() { if j.VaultToken == nil { j.VaultToken = stringToPtr("") } + if j.VaultNamespace == nil { + j.VaultNamespace = stringToPtr("") + } if j.NomadTokenID == nil { j.NomadTokenID = stringToPtr("") } diff --git a/vendor/github.com/hashicorp/nomad/api/tasks.go b/vendor/github.com/hashicorp/nomad/api/tasks.go index b9b79af5470..8275813e6ee 100644 --- a/vendor/github.com/hashicorp/nomad/api/tasks.go +++ b/vendor/github.com/hashicorp/nomad/api/tasks.go @@ -812,6 +812,7 @@ func (tmpl *Template) Canonicalize() { type Vault struct { Policies []string + Namespace *string `mapstructure:"namespace"` Env *bool ChangeMode *string `mapstructure:"change_mode"` ChangeSignal *string `mapstructure:"change_signal"` @@ -821,6 +822,9 @@ func (v *Vault) Canonicalize() { if v.Env == nil { v.Env = boolToPtr(true) } + if v.Namespace == nil { + v.Namespace = stringToPtr("") + } if v.ChangeMode == nil { v.ChangeMode = stringToPtr("restart") } diff --git a/website/pages/docs/job-specification/vault.mdx b/website/pages/docs/job-specification/vault.mdx index 996f3641abd..f823659c5c9 100644 --- a/website/pages/docs/job-specification/vault.mdx +++ b/website/pages/docs/job-specification/vault.mdx @@ -71,6 +71,10 @@ with Vault as well. - `env` `(bool: true)` - Specifies if the `VAULT_TOKEN` and `VAULT_NAMESPACE` environment variables should be set when starting the task. +- `namespace` `(string: "")` - Specifies the Vault Namespace + to use for the task. The Nomad client will retrieve a Vault token that is scoped to + this particular namespace. + - `policies` `(array: [])` - Specifies the set of Vault policies that the task requires. The Nomad client will retrieve a Vault token that is limited to those policies. @@ -106,6 +110,22 @@ vault { } ``` +### Vault Namespace + +This example shows specifying a particular Vault namespace for a given task. + + + +```hcl +vault { + policies = ["frontend"] + namespace = "engineering/frontend" + + change_mode = "signal" + change_signal = "SIGINT" +} +``` + [restart]: /docs/job-specification/restart 'Nomad restart Job Specification' [template]: /docs/job-specification/template 'Nomad template Job Specification' [vault]: https://www.vaultproject.io/ 'Vault by HashiCorp' diff --git a/website/pages/docs/upgrade/upgrade-specific.mdx b/website/pages/docs/upgrade/upgrade-specific.mdx index d26629a6585..3d3b315767c 100644 --- a/website/pages/docs/upgrade/upgrade-specific.mdx +++ b/website/pages/docs/upgrade/upgrade-specific.mdx @@ -15,6 +15,16 @@ details provided for their upgrades as a result of new features or changed behavior. This page is used to document those details separately from the standard upgrade flow. +## Nomad 0.12.2 + +### Vault Namespace Environment Variable + +Nomad 0.12.2 allows Enterprise users to specify a Vault Namespace for a +particular Job, Group, or Task. Before Nomad 0.12.2 the `VAULT_NAMESPACE` +environment variable was ignored when submitting jobs. Nomad 0.12.2 changes the +functionality and uses the `VAULT_NAMESPACE` environment variable when +submitting jobs. + ## Nomad 0.12.0 ### Enterprise Licensing