Skip to content

Commit

Permalink
cli: correctly use and validate job with vault token set
Browse files Browse the repository at this point in the history
This PR fixes `job validate` to respect '-vault-token', '$VAULT_TOKEN',
'-vault-namespace' if set.
  • Loading branch information
shoenig committed May 19, 2022
1 parent 3e8f52b commit 0a5992b
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .changelog/13070.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
cli: Fixed a bug where job validate did not respect vault token or namespace
```
50 changes: 46 additions & 4 deletions command/job_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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"),
}
}

Expand All @@ -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", "")

Expand Down Expand Up @@ -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 {
Expand Down
83 changes: 48 additions & 35 deletions command/job_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
defer s.Stop()

ui := cli.NewMockUi()
cmd := &JobValidateCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}}
// 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()

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("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)
})

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) {
Expand Down
17 changes: 17 additions & 0 deletions command/testdata/example-basic.nomad
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
14 changes: 14 additions & 0 deletions command/testdata/example-vault.nomad
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
job "vault" {
datacenters = ["dc1"]
group "group" {
task "task" {
driver = "docker"
config {
image = "alpine:latest"
}
vault {
policies = ["my-policy"]
}
}
}
}
4 changes: 2 additions & 2 deletions nomad/job_endpoint_hook_vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions testutil/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,7 +117,8 @@ func defaultServerConfig() (*TestServerConfig, []int) {
Enabled: false,
},
Vault: &VaultConfig{
Enabled: false,
Enabled: false,
AllowUnauthenticated: true,
},
ACL: &ACLConfig{
Enabled: false,
Expand Down
20 changes: 20 additions & 0 deletions website/content/docs/commands/job/validate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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=<key=value>`: Variable for template, can be used multiple times.

- `-var-file=<path>`: Path to HCL2 file containing user variables.
Expand Down Expand Up @@ -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

0 comments on commit 0a5992b

Please sign in to comment.