diff --git a/.changelog/13070.txt b/.changelog/13070.txt new file mode 100644 index 000000000..b072ff39a --- /dev/null +++ b/.changelog/13070.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: Fixed a bug where job validate did not respect vault token or namespace +``` diff --git a/command/job_validate.go b/command/job_validate.go index 52ae88293..ba60e8cc5 100644 --- a/command/job_validate.go +++ b/command/job_validate.go @@ -2,11 +2,13 @@ package command import ( "fmt" + "os" "strings" "github.com/hashicorp/go-multierror" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/command/agent" + "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/structs" "github.com/posener/complete" ) @@ -28,6 +30,10 @@ Alias: nomad validate it is read from the file at the supplied path or downloaded and read from URL specified. + The run command will set the vault_token of the job based on the following + precedence, going from highest to lowest: the -vault-token flag, the + $VAULT_TOKEN environment variable and finally the value in the job file. + When ACLs are enabled, this command requires a token with the 'read-job' capability for the job's namespace. @@ -50,6 +56,22 @@ Validate Options: has been supplied which is not defined within the root variables. Defaults to true. + -vault-token + Used to validate if the user submitting the job has permission to run the job + according to its Vault policies. A Vault token must be supplied if the vault + stanza allow_unauthenticated is disabled in the Nomad server configuration. + If the -vault-token flag is set, the passed Vault token is added to the jobspec + before sending to the Nomad servers. This allows passing the Vault token + without storing it in the job file. This overrides the token found in the + $VAULT_TOKEN environment variable and the vault_token field in the job file. + This token is cleared from the job after validating and cannot be used within + the job executing environment. Use the vault stanza when templating in a job + with a Vault token. + + -vault-namespace + If set, the passed Vault namespace is stored in the job before sending to the + Nomad servers. + -var 'key=value' Variable for template, can be used multiple times. @@ -65,10 +87,12 @@ func (c *JobValidateCommand) Synopsis() string { func (c *JobValidateCommand) AutocompleteFlags() complete.Flags { return complete.Flags{ - "-hcl1": complete.PredictNothing, - "-hcl2-strict": complete.PredictNothing, - "-var": complete.PredictAnything, - "-var-file": complete.PredictFiles("*.var"), + "-hcl1": complete.PredictNothing, + "-hcl2-strict": complete.PredictNothing, + "-vault-token": complete.PredictAnything, + "-vault-namespace": complete.PredictAnything, + "-var": complete.PredictAnything, + "-var-file": complete.PredictFiles("*.var"), } } @@ -83,11 +107,15 @@ func (c *JobValidateCommand) AutocompleteArgs() complete.Predictor { func (c *JobValidateCommand) Name() string { return "job validate" } func (c *JobValidateCommand) Run(args []string) int { + var vaultToken, vaultNamespace string + flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) flagSet.Usage = func() { c.Ui.Output(c.Help()) } flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "") flagSet.BoolVar(&c.JobGetter.HCL1, "hcl1", false, "") flagSet.BoolVar(&c.JobGetter.Strict, "hcl2-strict", true, "") + flagSet.StringVar(&vaultToken, "vault-token", "", "") + flagSet.StringVar(&vaultNamespace, "vault-namespace", "", "") flagSet.Var(&c.JobGetter.Vars, "var", "") flagSet.Var(&c.JobGetter.VarFiles, "var-file", "") @@ -127,6 +155,20 @@ func (c *JobValidateCommand) Run(args []string) int { client.SetRegion(*r) } + // Parse the Vault token + if vaultToken == "" { + // Check the environment variable + vaultToken = os.Getenv("VAULT_TOKEN") + } + + if vaultToken != "" { + job.VaultToken = helper.StringToPtr(vaultToken) + } + + if vaultNamespace != "" { + job.VaultNamespace = helper.StringToPtr(vaultNamespace) + } + // Check that the job is valid jr, _, err := client.Jobs().Validate(job, nil) if err != nil { diff --git a/command/job_validate_test.go b/command/job_validate_test.go index 25f2f3063..71f683734 100644 --- a/command/job_validate_test.go +++ b/command/job_validate_test.go @@ -17,44 +17,57 @@ func TestValidateCommand_Implements(t *testing.T) { var _ cli.Command = &JobValidateCommand{} } -func TestValidateCommand(t *testing.T) { +func TestValidateCommand_Files(t *testing.T) { ci.Parallel(t) - // Create a server - s := testutil.NewTestServer(t, nil) + + // Create a Vault server + v := testutil.NewTestVault(t) + defer v.Stop() + + // Create a Nomad server + s := testutil.NewTestServer(t, func(c *testutil.TestServerConfig) { + c.Vault.Address = v.HTTPAddr + c.Vault.Enabled = true + c.Vault.AllowUnauthenticated = false + c.Vault.Token = v.RootToken + }) defer s.Stop() - ui := cli.NewMockUi() - cmd := &JobValidateCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}} + t.Run("basic", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobValidateCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}} + args := []string{"testdata/example-basic.nomad"} + code := cmd.Run(args) + require.Equal(t, 0, code) + }) - fh, err := ioutil.TempFile("", "nomad") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.Remove(fh.Name()) - _, err = fh.WriteString(` -job "job1" { - type = "service" - datacenters = [ "dc1" ] - group "group1" { - count = 1 - task "task1" { - driver = "exec" - config { - command = "/bin/sleep" - } - resources { - cpu = 1000 - memory = 512 - } - } - } -}`) - if err != nil { - t.Fatalf("err: %s", err) - } - if code := cmd.Run([]string{fh.Name()}); code != 0 { - t.Fatalf("expect exit 0, got: %d: %s", code, ui.ErrorWriter.String()) - } + t.Run("vault no token", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobValidateCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} + code := cmd.Run(args) + require.Contains(t, ui.ErrorWriter.String(), "* Vault used in the job but missing Vault token") + require.Equal(t, 1, code) + }) + + t.Run("vault bad token via flag", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobValidateCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "-vault-token=abc123", "testdata/example-vault.nomad"} + code := cmd.Run(args) + require.Contains(t, ui.ErrorWriter.String(), "* bad token") + require.Equal(t, 1, code) + }) + + t.Run("vault token bad via env", func(t *testing.T) { + t.Setenv("VAULT_TOKEN", "abc123") + ui := cli.NewMockUi() + cmd := &JobValidateCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} + code := cmd.Run(args) + require.Contains(t, ui.ErrorWriter.String(), "* bad token") + require.Equal(t, 1, code) + }) } func TestValidateCommand_Fails(t *testing.T) { diff --git a/command/testdata/example-basic.nomad b/command/testdata/example-basic.nomad new file mode 100644 index 000000000..bd3727eaa --- /dev/null +++ b/command/testdata/example-basic.nomad @@ -0,0 +1,17 @@ +job "job1" { + type = "service" + datacenters = ["dc1"] + group "group1" { + count = 1 + task "task1" { + driver = "exec" + config { + command = "/bin/sleep" + } + resources { + cpu = 1000 + memory = 512 + } + } + } +} diff --git a/command/testdata/example-vault.nomad b/command/testdata/example-vault.nomad new file mode 100644 index 000000000..a2873e4df --- /dev/null +++ b/command/testdata/example-vault.nomad @@ -0,0 +1,14 @@ +job "vault" { + datacenters = ["dc1"] + group "group" { + task "task" { + driver = "docker" + config { + image = "alpine:latest" + } + vault { + policies = ["my-policy"] + } + } + } +} diff --git a/nomad/job_endpoint_hook_vault.go b/nomad/job_endpoint_hook_vault.go index 50a667342..f92bbdd9f 100644 --- a/nomad/job_endpoint_hook_vault.go +++ b/nomad/job_endpoint_hook_vault.go @@ -10,7 +10,7 @@ import ( vapi "github.com/hashicorp/vault/api" ) -// jobVaultHook is an job registration admission controllver for Vault blocks. +// jobVaultHook is an job registration admission controller for Vault blocks. type jobVaultHook struct { srv *Server } @@ -62,7 +62,7 @@ func (h jobVaultHook) Validate(job *structs.Job) ([]error, error) { } // validatePolicies returns an error if the job contains Vault blocks that -// require policies that the requirest token is not allowed to access. +// require policies that the request token is not allowed to access. func (jobVaultHook) validatePolicies( blocks map[string]map[string]*structs.Vault, token *vapi.Secret, diff --git a/testutil/server.go b/testutil/server.go index 5c83b4e69..8f6e4da3b 100644 --- a/testutil/server.go +++ b/testutil/server.go @@ -81,7 +81,10 @@ type ClientConfig struct { // VaultConfig is used to configure Vault type VaultConfig struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` + Address string `json:"address"` + AllowUnauthenticated bool `json:"allow_unauthenticated"` + Token string `json:"token"` } // ACLConfig is used to configure ACLs @@ -114,7 +117,8 @@ func defaultServerConfig() (*TestServerConfig, []int) { Enabled: false, }, Vault: &VaultConfig{ - Enabled: false, + Enabled: false, + AllowUnauthenticated: true, }, ACL: &ACLConfig{ Enabled: false, diff --git a/website/content/docs/commands/job/validate.mdx b/website/content/docs/commands/job/validate.mdx index 057443990..88a262bc5 100644 --- a/website/content/docs/commands/job/validate.mdx +++ b/website/content/docs/commands/job/validate.mdx @@ -29,6 +29,10 @@ supports `go-getter` syntax. On successful validation, exit code 0 will be returned, otherwise an exit code of 1 indicates an error. +The run command will set the `vault_token` of the job based on the following +precedence, going from highest to lowest: the `-vault-token` flag, the +`$VAULT_TOKEN` environment variable and finally the value in the job file. + When ACLs are enabled, this command requires a token with the `read-job` capability for the job's namespace. @@ -48,6 +52,20 @@ capability for the job's namespace. a variable has been supplied which is not defined within the root variables. Defaults to true. +- `-vault-token`: Used to validate if the user submitting the job has + permission to run the job according to its Vault policies. A Vault token must + be supplied if the [`vault` stanza `allow_unauthenticated`] is disabled in + the Nomad server configuration. If the `-vault-token` flag is set, the passed + Vault token is added to the jobspec before sending to the Nomad servers. This + allows passing the Vault token without storing it in the job file. This + overrides the token found in the `$VAULT_TOKEN` environment variable and the + [`vault_token`] field in the job file. This token is cleared from the job + after validating and cannot be used within the job executing environment. Use + the `vault` stanza when templating in a job with a Vault token. + +- `-vault-namespace`: If set, the passed Vault namespace is stored in the job + before sending to the Nomad servers. + - `-var=`: Variable for template, can be used multiple times. - `-var-file=`: Path to HCL2 file containing user variables. @@ -79,3 +97,5 @@ Job validation successful [`go-getter`]: https://github.com/hashicorp/go-getter [job specification]: /docs/job-specification +[`vault` stanza `allow_unauthenticated`]: /docs/configuration/vault#allow_unauthenticated +[`vault_token`]: /docs/job-specification/job#vault_token \ No newline at end of file