open-nomad/nomad/job_endpoint_hook_vault.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
}