ab7eb5de6e
Move some common Vault API data struct decoding out of the Vault client so it can be reused in other situations. Make Vault job validation its own function so it's easier to expand it. Rename the `Job.VaultPolicies` method to just `Job.Vault` since it returns the full Vault block, not just their policies. Set `ChangeMode` on `Vault.Canonicalize`. Add some missing tests. Allows specifying an entity alias that will be used by Nomad when deriving the task Vault token. An entity alias assigns an indentity to a token, allowing better control and management of Vault clients since all tokens with the same indentity alias will now be considered the same client. This helps track Nomad activity in Vault's audit logs and better control over Vault billing. Add support for a new Nomad server configuration to define a default entity alias to be used when deriving Vault tokens. This default value will be used if the task doesn't have an entity alias defined.
188 lines
5.2 KiB
Go
188 lines
5.2 KiB
Go
package nomad
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/nomad/helper"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
vapi "github.com/hashicorp/vault/api"
|
|
)
|
|
|
|
// jobVaultHook is an job registration admission controllver for Vault blocks.
|
|
type jobVaultHook struct {
|
|
srv *Server
|
|
}
|
|
|
|
func (jobVaultHook) Name() string {
|
|
return "vault"
|
|
}
|
|
|
|
func (h jobVaultHook) Validate(job *structs.Job) ([]error, error) {
|
|
vaultBlocks := job.Vault()
|
|
if len(vaultBlocks) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
vconf := h.srv.config.VaultConfig
|
|
if !vconf.IsEnabled() {
|
|
return nil, fmt.Errorf("Vault not enabled but used in the job")
|
|
}
|
|
|
|
// Return early if Vault configuration doesn't require authentication.
|
|
if vconf.AllowsUnauthenticated() {
|
|
return nil, nil
|
|
}
|
|
|
|
// At this point the job has a vault block and the server requires
|
|
// authentication, so check if the user has the right permissions.
|
|
if job.VaultToken == "" {
|
|
return nil, fmt.Errorf("Vault used in the job but missing Vault token")
|
|
}
|
|
|
|
tokenSecret, err := h.srv.vault.LookupToken(context.Background(), job.VaultToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to lookup Vault token: %v", err)
|
|
}
|
|
|
|
// Check namespaces.
|
|
err = h.validateNamespaces(vaultBlocks, tokenSecret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check policies.
|
|
err = h.validatePolicies(vaultBlocks, tokenSecret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check entity aliases.
|
|
err = h.validateEntityAliases(vaultBlocks, tokenSecret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// validatePolicies returns an error if the job contains Vault blocks that
|
|
// require policies that the requirest token is not allowed to access.
|
|
func (jobVaultHook) validatePolicies(
|
|
blocks map[string]map[string]*structs.Vault,
|
|
token *vapi.Secret,
|
|
) error {
|
|
|
|
jobPolicies := structs.VaultPoliciesSet(blocks)
|
|
if len(jobPolicies) == 0 {
|
|
return nil
|
|
}
|
|
|
|
allowedPolicies, err := token.TokenPolicies()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to lookup Vault token policies: %v", err)
|
|
}
|
|
|
|
// If we are given a root token it can access all policies
|
|
if helper.SliceStringContains(allowedPolicies, "root") {
|
|
return nil
|
|
}
|
|
|
|
subset, offending := helper.SliceStringIsSubset(allowedPolicies, jobPolicies)
|
|
if !subset {
|
|
return fmt.Errorf("Vault token doesn't allow access to the following policies: %s",
|
|
strings.Join(offending, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateEntityAliases returns an error if the job contains Vault blocks that
|
|
// use an entity alias that are not allowed to be used.
|
|
//
|
|
// In order to use entity aliases in a job, the following conditions must
|
|
// be met:
|
|
// - the token used to submit the job and the Nomad server configuration
|
|
// must have a role
|
|
// - both roles must allow access to all entity aliases defined in the job
|
|
//
|
|
// If the Nomad server is configured with a default entity alias, it will
|
|
// use that for any Vault block that don't specify one, so:
|
|
// - the token used to submit the job must be allowed to use the default
|
|
// entity alias
|
|
// - except if all Vault blocks in the job define an alias, since in this
|
|
// case the server alias would not be used.
|
|
func (h jobVaultHook) validateEntityAliases(
|
|
blocks map[string]map[string]*structs.Vault,
|
|
token *vapi.Secret,
|
|
) error {
|
|
|
|
// Assign the default entity alias from the server to any vault block with
|
|
// no entity alias already set
|
|
vconf := h.srv.config.VaultConfig
|
|
if vconf.EntityAlias != "" {
|
|
for _, task := range blocks {
|
|
for _, v := range task {
|
|
if v.EntityAlias == "" {
|
|
v.EntityAlias = vconf.EntityAlias
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
aliases := structs.VaultEntityAliasesSet(blocks)
|
|
if len(aliases) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var tokenData structs.VaultTokenData
|
|
if err := structs.DecodeVaultSecretData(token, &tokenData); err != nil {
|
|
return fmt.Errorf("failed to parse Vault token data: %v", err)
|
|
}
|
|
|
|
// Check if user token allows requested entity aliases.
|
|
if tokenData.Role == "" {
|
|
return fmt.Errorf("jobs with Vault entity aliases require the Vault token to have a role")
|
|
}
|
|
if err := h.validateRole(tokenData.Role, aliases); err != nil {
|
|
return fmt.Errorf("failed to validate entity alias against Vault token: %v", err)
|
|
}
|
|
|
|
// Check if Nomad server role allows requested entity aliases.
|
|
if vconf.Role == "" {
|
|
return fmt.Errorf("jobs with Vault entity aliases require the Nomad server to have a Vault role")
|
|
}
|
|
if err := h.validateRole(vconf.Role, aliases); err != nil {
|
|
return fmt.Errorf("failed to validate entity alias against Nomad server configuration: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateRole returns an error if the given role doesn't allow some of the
|
|
// aliases to be used.
|
|
func (h jobVaultHook) validateRole(role string, aliases []string) error {
|
|
s, err := h.srv.vault.LookupTokenRole(context.Background(), role)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var data structs.VaultTokenRoleData
|
|
if err := structs.DecodeVaultSecretData(s, &data); err != nil {
|
|
return fmt.Errorf("failed to parse role data: %v", err)
|
|
}
|
|
|
|
invalidAliases := []string{}
|
|
for _, a := range aliases {
|
|
if !data.AllowsEntityAlias(a) {
|
|
invalidAliases = append(invalidAliases, a)
|
|
}
|
|
}
|
|
if len(invalidAliases) > 0 {
|
|
return fmt.Errorf("role doesn't allow access to the following entity aliases: %s",
|
|
strings.Join(invalidAliases, ", "))
|
|
}
|
|
return nil
|
|
}
|