open-nomad/nomad/job_endpoint_hook_vault.go
Luiz Aoqui ab7eb5de6e
Support Vault entity aliases (#12449)
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.
2022-04-05 14:18:10 -04:00

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
}