cli: correctly use and validate job with vault token set

This PR fixes `job validate` to respect '-vault-token', '$VAULT_TOKEN',
'-vault-namespace' if set.
This commit is contained in:
Seth Hoenig 2022-05-19 11:18:06 -05:00
parent b72ff42ada
commit fc58f4972c
8 changed files with 155 additions and 42 deletions

3
.changelog/13070.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
cli: Fixed a bug where job validate did not respect vault token or namespace
```

View File

@ -2,11 +2,13 @@ package command
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/command/agent" "github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/posener/complete" "github.com/posener/complete"
) )
@ -28,6 +30,10 @@ Alias: nomad validate
it is read from the file at the supplied path or downloaded and it is read from the file at the supplied path or downloaded and
read from URL specified. 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' When ACLs are enabled, this command requires a token with the 'read-job'
capability for the job's namespace. capability for the job's namespace.
@ -50,6 +56,22 @@ Validate Options:
has been supplied which is not defined within the root variables. Defaults has been supplied which is not defined within the root variables. Defaults
to true. 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' -var 'key=value'
Variable for template, can be used multiple times. Variable for template, can be used multiple times.
@ -65,10 +87,12 @@ func (c *JobValidateCommand) Synopsis() string {
func (c *JobValidateCommand) AutocompleteFlags() complete.Flags { func (c *JobValidateCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{ return complete.Flags{
"-hcl1": complete.PredictNothing, "-hcl1": complete.PredictNothing,
"-hcl2-strict": complete.PredictNothing, "-hcl2-strict": complete.PredictNothing,
"-var": complete.PredictAnything, "-vault-token": complete.PredictAnything,
"-var-file": complete.PredictFiles("*.var"), "-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) Name() string { return "job validate" }
func (c *JobValidateCommand) Run(args []string) int { func (c *JobValidateCommand) Run(args []string) int {
var vaultToken, vaultNamespace string
flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient) flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient)
flagSet.Usage = func() { c.Ui.Output(c.Help()) } flagSet.Usage = func() { c.Ui.Output(c.Help()) }
flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "") flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "")
flagSet.BoolVar(&c.JobGetter.HCL1, "hcl1", false, "") flagSet.BoolVar(&c.JobGetter.HCL1, "hcl1", false, "")
flagSet.BoolVar(&c.JobGetter.Strict, "hcl2-strict", true, "") 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.Vars, "var", "")
flagSet.Var(&c.JobGetter.VarFiles, "var-file", "") flagSet.Var(&c.JobGetter.VarFiles, "var-file", "")
@ -127,6 +155,20 @@ func (c *JobValidateCommand) Run(args []string) int {
client.SetRegion(*r) 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 // Check that the job is valid
jr, _, err := client.Jobs().Validate(job, nil) jr, _, err := client.Jobs().Validate(job, nil)
if err != nil { if err != nil {

View File

@ -17,44 +17,57 @@ func TestValidateCommand_Implements(t *testing.T) {
var _ cli.Command = &JobValidateCommand{} var _ cli.Command = &JobValidateCommand{}
} }
func TestValidateCommand(t *testing.T) { func TestValidateCommand_Files(t *testing.T) {
ci.Parallel(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() defer s.Stop()
ui := cli.NewMockUi() t.Run("basic", func(t *testing.T) {
cmd := &JobValidateCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}} 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") t.Run("vault no token", func(t *testing.T) {
if err != nil { ui := cli.NewMockUi()
t.Fatalf("err: %s", err) cmd := &JobValidateCommand{Meta: Meta{Ui: ui}}
} args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"}
defer os.Remove(fh.Name()) code := cmd.Run(args)
_, err = fh.WriteString(` require.Contains(t, ui.ErrorWriter.String(), "* Vault used in the job but missing Vault token")
job "job1" { require.Equal(t, 1, code)
type = "service" })
datacenters = [ "dc1" ]
group "group1" { t.Run("vault bad token via flag", func(t *testing.T) {
count = 1 ui := cli.NewMockUi()
task "task1" { cmd := &JobValidateCommand{Meta: Meta{Ui: ui}}
driver = "exec" args := []string{"-address", "http://" + s.HTTPAddr, "-vault-token=abc123", "testdata/example-vault.nomad"}
config { code := cmd.Run(args)
command = "/bin/sleep" require.Contains(t, ui.ErrorWriter.String(), "* bad token")
} require.Equal(t, 1, code)
resources { })
cpu = 1000
memory = 512 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"}
if err != nil { code := cmd.Run(args)
t.Fatalf("err: %s", err) require.Contains(t, ui.ErrorWriter.String(), "* bad token")
} require.Equal(t, 1, code)
if code := cmd.Run([]string{fh.Name()}); code != 0 { })
t.Fatalf("expect exit 0, got: %d: %s", code, ui.ErrorWriter.String())
}
} }
func TestValidateCommand_Fails(t *testing.T) { func TestValidateCommand_Fails(t *testing.T) {

17
command/testdata/example-basic.nomad vendored Normal file
View File

@ -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
command/testdata/example-vault.nomad vendored Normal file
View File

@ -0,0 +1,14 @@
job "vault" {
datacenters = ["dc1"]
group "group" {
task "task" {
driver = "docker"
config {
image = "alpine:latest"
}
vault {
policies = ["my-policy"]
}
}
}
}

View File

@ -10,7 +10,7 @@ import (
vapi "github.com/hashicorp/vault/api" 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 { type jobVaultHook struct {
srv *Server 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 // 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( func (jobVaultHook) validatePolicies(
blocks map[string]map[string]*structs.Vault, blocks map[string]map[string]*structs.Vault,
token *vapi.Secret, token *vapi.Secret,

View File

@ -81,7 +81,10 @@ type ClientConfig struct {
// VaultConfig is used to configure Vault // VaultConfig is used to configure Vault
type VaultConfig struct { 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 // ACLConfig is used to configure ACLs
@ -114,7 +117,8 @@ func defaultServerConfig() (*TestServerConfig, []int) {
Enabled: false, Enabled: false,
}, },
Vault: &VaultConfig{ Vault: &VaultConfig{
Enabled: false, Enabled: false,
AllowUnauthenticated: true,
}, },
ACL: &ACLConfig{ ACL: &ACLConfig{
Enabled: false, Enabled: false,

View File

@ -29,6 +29,10 @@ supports `go-getter` syntax.
On successful validation, exit code 0 will be returned, otherwise an exit code On successful validation, exit code 0 will be returned, otherwise an exit code
of 1 indicates an error. 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` When ACLs are enabled, this command requires a token with the `read-job`
capability for the job's namespace. 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. a variable has been supplied which is not defined within the root variables.
Defaults to true. 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=<key=value>`: Variable for template, can be used multiple times.
- `-var-file=<path>`: Path to HCL2 file containing user variables. - `-var-file=<path>`: Path to HCL2 file containing user variables.
@ -79,3 +97,5 @@ Job validation successful
[`go-getter`]: https://github.com/hashicorp/go-getter [`go-getter`]: https://github.com/hashicorp/go-getter
[job specification]: /docs/job-specification [job specification]: /docs/job-specification
[`vault` stanza `allow_unauthenticated`]: /docs/configuration/vault#allow_unauthenticated
[`vault_token`]: /docs/job-specification/job#vault_token