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
|
|
}
|