Merge pull request #13070 from hashicorp/b-vault-validator-env
cli: correctly validate job with vault token set
This commit is contained in:
commit
d9c10fccde
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
cli: Fixed a bug where job validate did not respect vault token or namespace
|
||||
```
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
job "vault" {
|
||||
datacenters = ["dc1"]
|
||||
group "group" {
|
||||
task "task" {
|
||||
driver = "docker"
|
||||
config {
|
||||
image = "alpine:latest"
|
||||
}
|
||||
vault {
|
||||
policies = ["my-policy"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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=<key=value>`: Variable for template, can be used multiple times.
|
||||
|
||||
- `-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
|
||||
[job specification]: /docs/job-specification
|
||||
[`vault` stanza `allow_unauthenticated`]: /docs/configuration/vault#allow_unauthenticated
|
||||
[`vault_token`]: /docs/job-specification/job#vault_token
|
Loading…
Reference in New Issue