From e96d52d87f46814e2a8f7e99509fea9112aa93b4 Mon Sep 17 00:00:00 2001 From: Seth Hoenig Date: Thu, 11 Aug 2022 08:51:45 -0500 Subject: [PATCH] cli: respect vault token in plan command This PR fixes a regression where the 'job plan' command would not respect a Vault token if set via --vault-token or $VAULT_TOKEN. Basically the same bug/fix as for the validate command in https://github.com/hashicorp/nomad/issues/13062 Fixes https://github.com/hashicorp/nomad/issues/13939 --- .changelog/14088.txt | 3 ++ command/job_plan.go | 43 +++++++++++++++++ command/job_plan_test.go | 55 ++++++++++++++++++++++ website/content/docs/commands/job/plan.mdx | 20 ++++++++ 4 files changed, 121 insertions(+) create mode 100644 .changelog/14088.txt diff --git a/.changelog/14088.txt b/.changelog/14088.txt new file mode 100644 index 00000000000..e8963029aaa --- /dev/null +++ b/.changelog/14088.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: Fixed a bug where vault token not respected in plan command +``` diff --git a/command/job_plan.go b/command/job_plan.go index 540f5363249..c13f78b38bf 100644 --- a/command/job_plan.go +++ b/command/job_plan.go @@ -2,11 +2,13 @@ package command import ( "fmt" + "os" "sort" "strings" "time" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/scheduler" "github.com/posener/complete" ) @@ -62,6 +64,10 @@ Alias: nomad plan * 1: Allocations created or destroyed. * 255: Error determining plan results. + The plan 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 'submit-job' capability for the job's namespace. @@ -91,6 +97,22 @@ Plan Options: -policy-override Sets the flag to force override any soft mandatory Sentinel policies. + -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. @@ -116,6 +138,8 @@ func (c *JobPlanCommand) AutocompleteFlags() complete.Flags { "-json": complete.PredictNothing, "-hcl1": complete.PredictNothing, "-hcl2-strict": complete.PredictNothing, + "-vault-token": complete.PredictAnything, + "-vault-namespace": complete.PredictAnything, "-var": complete.PredictAnything, "-var-file": complete.PredictFiles("*.var"), }) @@ -132,6 +156,7 @@ func (c *JobPlanCommand) AutocompleteArgs() complete.Predictor { func (c *JobPlanCommand) Name() string { return "job plan" } func (c *JobPlanCommand) Run(args []string) int { var diff, policyOverride, verbose bool + var vaultToken, vaultNamespace string flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) flagSet.Usage = func() { c.Ui.Output(c.Help()) } @@ -141,6 +166,8 @@ func (c *JobPlanCommand) Run(args []string) int { 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", "") @@ -186,6 +213,22 @@ func (c *JobPlanCommand) Run(args []string) int { client.SetNamespace(*n) } + // Parse the Vault token. + if vaultToken == "" { + // Check the environment variable + vaultToken = os.Getenv("VAULT_TOKEN") + } + + // Set the vault token. + if vaultToken != "" { + job.VaultToken = pointer.Of(vaultToken) + } + + // Set the vault namespace. + if vaultNamespace != "" { + job.VaultNamespace = pointer.Of(vaultNamespace) + } + // Setup the options opts := &api.PlanOptions{} if diff { diff --git a/command/job_plan_test.go b/command/job_plan_test.go index 4ef7a861d93..74f20f8797f 100644 --- a/command/job_plan_test.go +++ b/command/job_plan_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + "github.com/shoenig/test/must" "github.com/stretchr/testify/require" ) @@ -156,6 +157,60 @@ job "job1" { ui.ErrorWriter.Reset() } +func TestPlanCommand_From_Files(t *testing.T) { + ci.Parallel(t) + + // 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() + + t.Run("fail to place", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-basic.nomad"} + code := cmd.Run(args) + require.Equal(t, 1, code) // no client running, fail to place + must.StrContains(t, ui.OutputWriter.String(), "WARNING: Failed to place all allocations.") + }) + + t.Run("vault no token", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} + code := cmd.Run(args) + must.Eq(t, 255, code) + must.StrContains(t, ui.ErrorWriter.String(), "* Vault used in the job but missing Vault token") + }) + + t.Run("vault bad token via flag", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "-vault-token=abc123", "testdata/example-vault.nomad"} + code := cmd.Run(args) + must.Eq(t, 255, code) + must.StrContains(t, ui.ErrorWriter.String(), "* bad token") + }) + + t.Run("vault bad token via env", func(t *testing.T) { + t.Setenv("VAULT_TOKEN", "abc123") + ui := cli.NewMockUi() + cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} + args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} + code := cmd.Run(args) + must.Eq(t, 255, code) + must.StrContains(t, ui.ErrorWriter.String(), "* bad token") + }) +} + func TestPlanCommand_From_URL(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() diff --git a/website/content/docs/commands/job/plan.mdx b/website/content/docs/commands/job/plan.mdx index 852a11fe4d0..68e63324664 100644 --- a/website/content/docs/commands/job/plan.mdx +++ b/website/content/docs/commands/job/plan.mdx @@ -48,6 +48,10 @@ Plan will return one of the following exit codes: - 1: Allocations created or destroyed. - 255: Error determining plan results. +The plan 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 `submit-job` capability for the job's namespace. @@ -73,6 +77,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 planning 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. @@ -241,3 +259,5 @@ if a change is detected. [`go-getter`]: https://github.com/hashicorp/go-getter [`nomad job run -check-index`]: /docs/commands/job/run#check-index [`tee`]: https://man7.org/linux/man-pages/man1/tee.1.html +[`vault` stanza `allow_unauthenticated`]: /docs/configuration/vault#allow_unauthenticated +[`vault_token`]: /docs/job-specification/job#vault_token