T3MP3ST public launch build
This commit is contained in:
1 parent
972247b881
commit
184c7d4e59
1589 files changed
+57150
-506
No files matched your search
@@ -0,0 +1,957 @@
|
||||
// Copyright IBM Corp. 2015, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
metrics "github.com/hashicorp/go-metrics/compat"
|
||||
"github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
// aclCacheSize is the number of ACL objects to keep cached. ACLs have a parsing
|
||||
// and construction cost, so we keep the hot objects cached to reduce the ACL
|
||||
// token resolution time.
|
||||
const aclCacheSize = 512
|
||||
|
||||
type StateGetter func() *state.StateStore
|
||||
type LeaderACLGetter func() string
|
||||
|
||||
type RPCContext interface {
|
||||
IsTLS() bool
|
||||
IsStatic() bool
|
||||
Certificate() *x509.Certificate
|
||||
GetRemoteIP() (net.IP, error)
|
||||
}
|
||||
|
||||
type Encrypter interface {
|
||||
VerifyClaim(string) (*structs.IdentityClaims, error)
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
aclsEnabled bool
|
||||
|
||||
// verifyTLS is used to determine whether the server should verify TLS and
|
||||
// is an atomic bool, so that the server TLS reload can update it at runtime
|
||||
// with a race condition.
|
||||
verifyTLS *atomic.Bool
|
||||
|
||||
logger hclog.Logger
|
||||
getState StateGetter
|
||||
getLeaderACL LeaderACLGetter
|
||||
region string
|
||||
|
||||
validServerCertNames []string
|
||||
validClientCertNames []string
|
||||
|
||||
// aclCache is used to maintain the parsed ACL objects
|
||||
aclCache *structs.ACLCache[*acl.ACL]
|
||||
|
||||
// encrypter is a pointer to the server's Encrypter that can be used to
|
||||
// verify claims
|
||||
encrypter Encrypter
|
||||
}
|
||||
|
||||
type AuthenticatorConfig struct {
|
||||
StateFn StateGetter
|
||||
Logger hclog.Logger
|
||||
GetLeaderACLFn LeaderACLGetter
|
||||
AclsEnabled bool
|
||||
VerifyTLS bool
|
||||
Region string
|
||||
Encrypter Encrypter
|
||||
}
|
||||
|
||||
func NewAuthenticator(cfg *AuthenticatorConfig) *Authenticator {
|
||||
a := Authenticator{
|
||||
aclsEnabled: cfg.AclsEnabled,
|
||||
verifyTLS: &atomic.Bool{},
|
||||
logger: cfg.Logger.With("auth"),
|
||||
getState: cfg.StateFn,
|
||||
getLeaderACL: cfg.GetLeaderACLFn,
|
||||
region: cfg.Region,
|
||||
aclCache: structs.NewACLCache[*acl.ACL](aclCacheSize),
|
||||
encrypter: cfg.Encrypter,
|
||||
validServerCertNames: []string{"server." + cfg.Region + ".nomad"},
|
||||
validClientCertNames: []string{
|
||||
"client." + cfg.Region + ".nomad",
|
||||
"server." + cfg.Region + ".nomad",
|
||||
},
|
||||
}
|
||||
|
||||
a.verifyTLS.Store(cfg.VerifyTLS)
|
||||
return &a
|
||||
}
|
||||
|
||||
// SetVerifyTLS is a helper method to set the verifyTLS field. This is used in
|
||||
// when the server TLS configuration is updated.
|
||||
func (s *Authenticator) SetVerifyTLS(verifyTLS bool) { s.verifyTLS.Store(verifyTLS) }
|
||||
|
||||
// Authenticate extracts an AuthenticatedIdentity from the request context or
|
||||
// provided token and sets the identity on the request. The caller can extract
|
||||
// an acl.ACL, WorkloadIdentity, or other identifying tokens to use for
|
||||
// authorization. Keeping these fields independent rather than merging them into
|
||||
// an ephemeral ACLToken makes the original of the credential clear to RPC
|
||||
// handlers, who may have different behavior for internal vs external origins.
|
||||
//
|
||||
// Note: when making a server-to-server RPC that authenticates with this method,
|
||||
// the RPC *must* include the leader's ACL token. Use AuthenticateServerOnly for
|
||||
// requests that don't have access to the leader's ACL token.
|
||||
//
|
||||
// Note: when called on the follower we'll be making stale queries, so it's
|
||||
// possible if the follower is behind that the leader will get a different value
|
||||
// if an ACL token or allocation's WI has just been created.
|
||||
//
|
||||
// This method returns errors that are used for testing diagnostics. RPC callers
|
||||
// should always return ErrPermissionDenied after checking forwarding when one
|
||||
// of these errors is received.
|
||||
func (s *Authenticator) Authenticate(ctx RPCContext, args structs.RequestWithIdentity) error {
|
||||
|
||||
// get the user ACLToken or anonymous token
|
||||
secretID := args.GetAuthToken()
|
||||
aclToken, err := s.resolveSecretToken(secretID)
|
||||
|
||||
switch {
|
||||
case err == nil && (aclToken == structs.AnonymousACLToken ||
|
||||
aclToken == structs.ACLsDisabledToken):
|
||||
// When ACLs are disabled or if we have an anonymous token, we want to
|
||||
// continue on to check mTLS certs, if available, so set the token but
|
||||
// don't return yet
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
|
||||
|
||||
case err == nil:
|
||||
// ACLs are enabled and we have a non-anonymous token, so set that as
|
||||
// our identity and return
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
|
||||
return nil
|
||||
|
||||
case errors.Is(err, structs.ErrTokenExpired):
|
||||
return err
|
||||
|
||||
case errors.Is(err, structs.ErrTokenInvalid):
|
||||
// if it's not a UUID it might be an identity claim
|
||||
claims, err := s.VerifyClaim(secretID)
|
||||
if err != nil {
|
||||
// we already know the token wasn't valid for an ACL in the state
|
||||
// store, so if we get an error at this point we have an invalid
|
||||
// token and there are no other options but to bail out
|
||||
return err
|
||||
}
|
||||
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{Claims: claims})
|
||||
return nil
|
||||
|
||||
case errors.Is(err, structs.ErrTokenNotFound):
|
||||
// Check if the secret ID is the leader's secret ID, in which case treat
|
||||
// it as a management token.
|
||||
leaderAcl := s.getLeaderACL()
|
||||
if leaderAcl != "" && secretID == leaderAcl {
|
||||
aclToken = structs.LeaderACLToken
|
||||
break
|
||||
} else {
|
||||
// Otherwise, see if the secret ID belongs to a node. We should
|
||||
// reach this point only on first connection.
|
||||
node, err := s.getState().NodeBySecretID(nil, secretID)
|
||||
if err != nil {
|
||||
// this is a go-memdb error; shouldn't happen
|
||||
return fmt.Errorf("could not resolve node secret: %w", err)
|
||||
}
|
||||
if node != nil {
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{ClientID: node.ID})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// we were passed a bogus token so we'll return an error, but we'll also
|
||||
// want to capture the IP for metrics
|
||||
remoteIP, err := ctx.GetRemoteIP()
|
||||
if err != nil {
|
||||
s.logger.Error("could not determine remote address", "error", err)
|
||||
}
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{RemoteIP: remoteIP})
|
||||
return structs.ErrPermissionDenied
|
||||
|
||||
default: // any other error
|
||||
return fmt.Errorf("could not resolve user: %w", err)
|
||||
|
||||
}
|
||||
|
||||
// If there's no context we're in a "static" handler which only happens for
|
||||
// cases where the leader is making RPCs internally (volumewatcher and
|
||||
// deploymentwatcher)
|
||||
if ctx.IsStatic() {
|
||||
args.SetIdentity(&structs.AuthenticatedIdentity{ACLToken: aclToken})
|
||||
return nil
|
||||
}
|
||||
|
||||
// At this point we either have an anonymous token or an invalid one.
|
||||
|
||||
// Unlike clients that provide their Node ID on first connection, server
|
||||
// RPCs don't include an ID for the server so we identify servers by cert
|
||||
// and IP address.
|
||||
identity := &structs.AuthenticatedIdentity{ACLToken: aclToken}
|
||||
if ctx.IsTLS() {
|
||||
identity.TLSName = ctx.Certificate().Subject.CommonName
|
||||
}
|
||||
|
||||
remoteIP, err := ctx.GetRemoteIP()
|
||||
if err != nil {
|
||||
s.logger.Error(
|
||||
"could not authenticate RPC request or determine remote address", "error", err)
|
||||
return err
|
||||
}
|
||||
identity.RemoteIP = remoteIP
|
||||
args.SetIdentity(identity)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveACL is an authentication wrapper that handles resolving ACL
|
||||
// tokens, Workload Identities, or client secrets into acl.ACL objects.
|
||||
// Exclusively server-to-server or client-to-server requests should be using
|
||||
// AuthenticateServerOnly or AuthenticateClientOnly unless they use the
|
||||
// AuthenticateNodeIdentityGenerator function.
|
||||
func (s *Authenticator) ResolveACL(args structs.RequestWithIdentity) (*acl.ACL, error) {
|
||||
identity := args.GetIdentity()
|
||||
if identity == nil {
|
||||
// should never happen
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !s.aclsEnabled {
|
||||
return acl.ACLsDisabledACL, nil
|
||||
}
|
||||
|
||||
if aclObj, err := s.ResolveClientIdentityACL(identity); aclObj != nil || err != nil {
|
||||
return aclObj, err
|
||||
}
|
||||
|
||||
claims := identity.GetClaims()
|
||||
if claims != nil {
|
||||
return s.resolveClaims(claims)
|
||||
}
|
||||
|
||||
// this will include any anonymous token, so this is the last chance to
|
||||
// avoid an error
|
||||
aclToken := identity.GetACLToken()
|
||||
if aclToken != nil {
|
||||
return s.resolveACLForToken(aclToken)
|
||||
}
|
||||
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// AuthenticateServerOnly returns an ACL object for use *only* with internal
|
||||
// server-to-server RPCs. This should never be used for RPCs that serve HTTP
|
||||
// endpoints or accept ACL tokens to avoid confused deputy attacks by making a
|
||||
// request to a follower that's forwarded.
|
||||
//
|
||||
// The returned ACL object is always an acl.ServerACL but in the future this
|
||||
// could be extended to allow servers to have jurisdiction over specific pools,
|
||||
// etc.
|
||||
func (s *Authenticator) AuthenticateServerOnly(ctx RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
|
||||
|
||||
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
|
||||
if err != nil {
|
||||
s.logger.Error("could not determine remote address", "error", err)
|
||||
}
|
||||
|
||||
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
|
||||
defer args.SetIdentity(identity) // always set the identity, even on errors
|
||||
|
||||
// Note: if servers had auth tokens like clients do, we would be able to
|
||||
// verify them here and only return the server ACL for actual servers even
|
||||
// if mTLS was disabled. Without mTLS, any request can spoof server RPCs.
|
||||
// This is known and documented in the Security Model:
|
||||
// https://developer.hashicorp.com/nomad/docs/secure/acl
|
||||
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validServerCertNames, identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return acl.ServerACL, nil
|
||||
}
|
||||
|
||||
// AuthenticateNodeIdentityGenerator is used for RPC endpoints (Node.Register
|
||||
// and Node.UpdateStatus) that have the potential to generate node identities.
|
||||
//
|
||||
// While the Authenticate method serves as a complete general purpose
|
||||
// authenticator, in some critical cases for identity generation checking, it
|
||||
// swallows the information needed.
|
||||
func (s *Authenticator) AuthenticateNodeIdentityGenerator(ctx RPCContext, args structs.RequestWithIdentity) error {
|
||||
|
||||
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
|
||||
if err != nil {
|
||||
s.logger.Error("could not determine remote address", "error", err)
|
||||
}
|
||||
|
||||
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
|
||||
defer args.SetIdentity(identity)
|
||||
|
||||
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validClientCertNames, identity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authToken := args.GetAuthToken()
|
||||
|
||||
// If the auth token is empty, we treat it as an anonymous request. In the
|
||||
// event of a node registration, this means the node is not yet registered.
|
||||
if authToken == "" {
|
||||
identity.ACLToken = structs.AnonymousACLToken
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the auth token is a UUID, we check whether it's a node secret ID or
|
||||
// the leader's ACL token. If it's not a UUID, we assume it's a node
|
||||
// identity. Anything outside these cases is not supported and no identity
|
||||
// will be set.
|
||||
if helper.IsUUID(authToken) {
|
||||
if leaderACL := s.getLeaderACL(); leaderACL != "" && authToken == leaderACL {
|
||||
identity.ACLToken = structs.LeaderACLToken
|
||||
} else {
|
||||
node, err := s.getState().NodeBySecretID(nil, authToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not resolve node secret: %w", err)
|
||||
}
|
||||
if node == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
identity.ClientID = node.ID
|
||||
}
|
||||
} else {
|
||||
// When verifying a node identity claim, we do not want to swallow the
|
||||
// initial error. This is because the caller may want to handle the
|
||||
// error type in the case that the JWT is expired.
|
||||
claims, err := s.VerifyClaim(authToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !claims.IsNode() && !claims.IsNodeIntroduction() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
identity.Claims = claims
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthenticateClientOnly returns an ACL object for use *only* with internal
|
||||
// RPCs originating from clients (including those forwarded). This should never
|
||||
// be used for RPCs that serve HTTP endpoints to avoid confused deputy attacks
|
||||
// by making a request to a client that's forwarded. It should also not be used
|
||||
// with Node.Register or NodeUpdateStatus, which should use
|
||||
// AuthenticateNodeIdentityGenerator.
|
||||
func (s *Authenticator) AuthenticateClientOnly(ctx RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
|
||||
|
||||
remoteIP, err := ctx.GetRemoteIP() // capture for metrics
|
||||
if err != nil {
|
||||
s.logger.Error("could not determine remote address", "error", err)
|
||||
}
|
||||
|
||||
identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP}
|
||||
defer args.SetIdentity(identity) // always set the identity, even on errors
|
||||
|
||||
if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validClientCertNames, identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authToken := args.GetAuthToken()
|
||||
if authToken == "" {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If the auth token is a UUID, we treat it as a node secret ID. Otherwise,
|
||||
// we assume it's a node identity claim. Anything outside these cases is not
|
||||
// permitted when using this method.
|
||||
if helper.IsUUID(authToken) {
|
||||
node, err := s.getState().NodeBySecretID(nil, authToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not resolve node secret: %w", err)
|
||||
}
|
||||
if node == nil {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
identity.ClientID = node.ID
|
||||
} else {
|
||||
claims, err := s.VerifyClaim(authToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !claims.IsNode() || claims.NodeIdentityClaims == nil {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
identity.ClientID = claims.NodeIdentityClaims.NodeID
|
||||
identity.Claims = claims
|
||||
}
|
||||
|
||||
return s.ResolveClientIdentityACL(identity)
|
||||
}
|
||||
|
||||
// verifyTLS is a helper function that performs TLS verification, if required,
|
||||
// given the passed RPCContext and valid names.
|
||||
//
|
||||
// It will always set the TLSName on the identity if we are performing
|
||||
// verification, so callers don't have to worry about setting it themselves.
|
||||
func verifyTLS(verify bool, ctx RPCContext, validNames []string, identity *structs.AuthenticatedIdentity) error {
|
||||
|
||||
if verify && !ctx.IsStatic() {
|
||||
|
||||
tlsCert := ctx.Certificate()
|
||||
if tlsCert == nil {
|
||||
return errors.New("missing certificate information")
|
||||
}
|
||||
|
||||
// Always set on the identity, even before validating the name, so we
|
||||
// can capture it for metrics.
|
||||
identity.TLSName = tlsCert.Subject.CommonName
|
||||
|
||||
// Perform the certificate validation, using the passed valid names.
|
||||
_, err := validateCertificateForNames(tlsCert, validNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertificateForNames returns true if the certificate is valid for any
|
||||
// of the given domain names.
|
||||
func validateCertificateForNames(cert *x509.Certificate, expectedNames []string) (bool, error) {
|
||||
if cert == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
validNames := []string{cert.Subject.CommonName}
|
||||
validNames = append(validNames, cert.DNSNames...)
|
||||
|
||||
for _, expectedName := range expectedNames {
|
||||
if slices.Contains(validNames, expectedName) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid certificate: %s not in expected %s",
|
||||
strings.Join(validNames, ", "),
|
||||
strings.Join(expectedNames, ", "))
|
||||
|
||||
}
|
||||
|
||||
// IdentityToACLClaim returns an ACLClaim suitable for checking permissions
|
||||
func IdentityToACLClaim(ai *structs.AuthenticatedIdentity, store *state.StateStore) *acl.ACLClaim {
|
||||
if ai == nil || ai.Claims == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var group string
|
||||
alloc, err := store.AllocByID(nil, ai.Claims.AllocationID)
|
||||
if err != nil {
|
||||
// we should never hit this error, but if we did the caller would get a
|
||||
// nil claim and auth will fail
|
||||
return nil
|
||||
}
|
||||
if alloc != nil {
|
||||
group = alloc.TaskGroup
|
||||
}
|
||||
|
||||
return &acl.ACLClaim{
|
||||
Namespace: ai.Claims.Namespace,
|
||||
Job: ai.Claims.JobID,
|
||||
Group: group,
|
||||
Task: ai.Claims.TaskName,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveACLForToken resolves an ACL from a token only. It should be used only
|
||||
// by Variables endpoints, which have additional implicit policies for their
|
||||
// claims so we can't wrap them up in ResolveACL.
|
||||
func (s *Authenticator) resolveACLForToken(aclToken *structs.ACLToken) (*acl.ACL, error) {
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resolveACLFromToken(snap, s.aclCache, aclToken)
|
||||
}
|
||||
|
||||
// ResolveToken is used to translate an ACL Token Secret ID into
|
||||
// an ACL object, nil if ACLs are disabled, or an error.
|
||||
//
|
||||
// TODO(tgross): this is used in lots of places we probably should be calling
|
||||
// Authenticate + ResolveACL on, because they support HTTP APIs that may be used
|
||||
// by the Task API
|
||||
func (s *Authenticator) ResolveToken(secretID string) (*acl.ACL, error) {
|
||||
// Fast-path if ACLs are disabled
|
||||
if !s.aclsEnabled {
|
||||
return acl.ACLsDisabledACL, nil
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "resolveToken"}, time.Now())
|
||||
|
||||
// Check if the secret ID is the leader secret ID, in which case treat it as
|
||||
// a management token.
|
||||
if leaderAcl := s.getLeaderACL(); leaderAcl != "" && secretID == leaderAcl {
|
||||
return acl.ManagementACL, nil
|
||||
}
|
||||
|
||||
// Snapshot the state
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve the ACL
|
||||
return resolveTokenFromSnapshotCache(snap, s.aclCache, secretID)
|
||||
}
|
||||
|
||||
// VerifyClaim asserts that the token is valid. If it is for a workload
|
||||
// identity, it will ensure that the resulting allocation ID belongs to a
|
||||
// non-terminal allocation. If the token is for a node identity, it will ensure
|
||||
// the node ID matches the claim.
|
||||
//
|
||||
// This should usually not be called by RPC handlers.
|
||||
func (s *Authenticator) VerifyClaim(token string) (*structs.IdentityClaims, error) {
|
||||
|
||||
claims, err := s.encrypter.VerifyClaim(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims.IsWorkload() {
|
||||
if err := s.verifyWorkloadIdentityClaim(claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// If the claims are for a node identity, we can return them directly once
|
||||
// we have verified the claim. In the happy path, we could read the node out
|
||||
// of state and verify that it is found. However, it is possible the node
|
||||
// has been garbage collected, and if we failed on that check, the node
|
||||
// would not be able to register again without manual intervention.
|
||||
if claims.IsNode() {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Node introduction claims are a case where we don't verify them against
|
||||
// the state store, since they are used to introduce a node that does not
|
||||
// yet exist.
|
||||
if claims.IsNodeIntroduction() {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("failed to determine claim type")
|
||||
}
|
||||
|
||||
// ResolveClientIdentityACL resolves a client ACL from an authenticated node
|
||||
// identity or client secret based identity. It returns (nil, nil) if the
|
||||
// identity does not represent a client.
|
||||
func (s *Authenticator) ResolveClientIdentityACL(identity *structs.AuthenticatedIdentity) (*acl.ACL, error) {
|
||||
if identity == nil || identity.ClientID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if claims := identity.GetClaims(); claims != nil {
|
||||
if !claims.IsNode() || claims.NodeIdentityClaims == nil || claims.NodeIdentityClaims.NodePool == "" {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
return acl.NewClientACL(claims.NodeIdentityClaims.NodePool), nil
|
||||
}
|
||||
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
node, err := snap.NodeByID(nil, identity.ClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node == nil {
|
||||
return nil, structs.ErrPermissionDenied
|
||||
}
|
||||
return acl.NewClientACL(node.NodePool), nil
|
||||
}
|
||||
|
||||
// AuthenticatedNodeID returns the authenticated node ID for client and node
|
||||
// identity based requests.
|
||||
func AuthenticatedNodeID(identity *structs.AuthenticatedIdentity) string {
|
||||
if identity == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if identity.ClientID != "" {
|
||||
return identity.ClientID
|
||||
}
|
||||
|
||||
claims := identity.GetClaims()
|
||||
if claims != nil && claims.IsNode() && claims.NodeIdentityClaims != nil {
|
||||
return claims.NodeIdentityClaims.NodeID
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// AuthorizeSameNode returns ErrPermissionDenied unless the authenticated
|
||||
// identity represents the same node as targetNodeID.
|
||||
func AuthorizeSameNode(identity *structs.AuthenticatedIdentity, targetNodeID string) error {
|
||||
if AuthenticatedNodeID(identity) != targetNodeID {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeSameNodeServiceRegistrations returns ErrPermissionDenied unless the
|
||||
// authenticated identity represents the same node as every service
|
||||
// registration's NodeID.
|
||||
func AuthorizeSameNodeServiceRegistrations(
|
||||
identity *structs.AuthenticatedIdentity,
|
||||
services []*structs.ServiceRegistration,
|
||||
) error {
|
||||
for _, service := range services {
|
||||
if service == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
if err := AuthorizeSameNode(identity, service.NodeID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveAuthorizedClientNodePoolByNodeID resolves the node pool for nodeID
|
||||
func resolveAuthorizedClientNodePoolByNodeID(snap *state.StateSnapshot, aclObj *acl.ACL, nodeID string) (string, error) {
|
||||
if nodeID == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
pool, ok, err := snap.NodePoolByNodeID(nil, nodeID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
if !aclObj.AllowClientOp(pool) {
|
||||
return "", structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// ResolveAuthorizedClientNodePoolByNodeID resolves the node pool for nodeID
|
||||
// and returns it if aclObj is authorized for that pool. It returns an empty
|
||||
// pool and nil error if the node does not exist yet, which allows callers to
|
||||
// continue handling blocking-query flows that may legitimately observe a
|
||||
// missing node.
|
||||
func (s *Authenticator) ResolveAuthorizedClientNodePoolByNodeID(aclObj *acl.ACL, nodeID string) (string, error) {
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resolveAuthorizedClientNodePoolByNodeID(snap, aclObj, nodeID)
|
||||
}
|
||||
|
||||
// AuthorizeClientAllocation returns ErrPermissionDenied unless aclObj is
|
||||
// authorized for alloc's node pool. If allowNsOp is provided, callers may
|
||||
// fall back to namespace-based authorization when client-scoped authorization
|
||||
// does not apply.
|
||||
func (s *Authenticator) AuthorizeClientAllocation(
|
||||
aclObj *acl.ACL,
|
||||
alloc *structs.Allocation,
|
||||
allowNsOp func(*acl.ACL, string) bool,
|
||||
) error {
|
||||
if alloc == nil || alloc.Job == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if aclObj.AllowClientOp(alloc.Job.NodePool) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if allowNsOp != nil && allowNsOp(aclObj, alloc.Namespace) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ResolveAuthorizedClientNodePoolByServiceRegistrationID resolves the node pool
|
||||
// for the node backing a service registration and returns it if aclObj is
|
||||
// authorized for that pool.
|
||||
func (s *Authenticator) ResolveAuthorizedClientNodePoolByServiceRegistrationID(
|
||||
aclObj *acl.ACL,
|
||||
namespace, id string,
|
||||
) (string, error) {
|
||||
if aclObj == nil || !(aclObj.AllowAgentRead() || aclObj.AllowServerOp() || aclObj.AllowNodeRead()) {
|
||||
return "", structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
registration, err := snap.GetServiceRegistrationByID(nil, namespace, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if registration == nil {
|
||||
return "", structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return resolveAuthorizedClientNodePoolByNodeID(snap, aclObj, registration.NodeID)
|
||||
}
|
||||
|
||||
func (s *Authenticator) verifyWorkloadIdentityClaim(claims *structs.IdentityClaims) error {
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alloc, err := snap.AllocByID(nil, claims.AllocationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alloc == nil || alloc.Job == nil {
|
||||
return fmt.Errorf("allocation does not exist")
|
||||
}
|
||||
|
||||
// the claims for terminal allocs are always treated as expired
|
||||
if alloc.ClientTerminalStatus() {
|
||||
return fmt.Errorf("allocation is terminal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Authenticator) resolveClaims(claims *structs.IdentityClaims) (*acl.ACL, error) {
|
||||
|
||||
// Nomad node identity claims currently map to a client ACL. If we open this
|
||||
// up in the future, we will want to modify this section to perform similar
|
||||
// work that is done for workload claims.
|
||||
if claims.IsNode() {
|
||||
if claims.NodeIdentityClaims == nil || claims.NodeIdentityClaims.NodePool == "" {
|
||||
return nil, fmt.Errorf("node identity claims missing node pool")
|
||||
}
|
||||
return acl.NewClientACL(claims.NodeIdentityClaims.NodePool), nil
|
||||
}
|
||||
|
||||
policies, err := s.ResolvePoliciesForClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compile and cache the ACL object. For many claims this will result in an
|
||||
// ACL object with no policies, which can be efficiently cached.
|
||||
aclObj, err := structs.CompileACLObject(s.aclCache, policies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aclObj, nil
|
||||
}
|
||||
|
||||
// resolveTokenFromSnapshotCache is used to resolve an ACL object from a
|
||||
// snapshot of state, using a cache to avoid parsing and ACL construction when
|
||||
// possible. It is split from resolveToken to simplify testing.
|
||||
func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *structs.ACLCache[*acl.ACL], secretID string) (*acl.ACL, error) {
|
||||
// Lookup the ACL Token
|
||||
var token *structs.ACLToken
|
||||
var err error
|
||||
|
||||
// Handle anonymous requests
|
||||
if secretID == "" {
|
||||
token = structs.AnonymousACLToken
|
||||
} else {
|
||||
token, err = snap.ACLTokenBySecretID(nil, secretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
}
|
||||
if token.IsExpired(time.Now().UTC()) {
|
||||
return nil, structs.ErrTokenExpired
|
||||
}
|
||||
}
|
||||
|
||||
return resolveACLFromToken(snap, cache, token)
|
||||
|
||||
}
|
||||
|
||||
func resolveACLFromToken(snap *state.StateSnapshot, cache *structs.ACLCache[*acl.ACL], token *structs.ACLToken) (*acl.ACL, error) {
|
||||
|
||||
// Check if this is a management token
|
||||
if token.Type == structs.ACLManagementToken {
|
||||
return acl.ManagementACL, nil
|
||||
}
|
||||
|
||||
// Store all policies detailed in the token request, this includes the
|
||||
// named policies and those referenced within the role link.
|
||||
policies := make([]*structs.ACLPolicy, 0, len(token.Policies)+len(token.Roles))
|
||||
|
||||
// Iterate all the token policies and add these to our policy tracking
|
||||
// array.
|
||||
for _, policyName := range token.Policies {
|
||||
policy, err := snap.ACLPolicyByName(nil, policyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if policy == nil {
|
||||
// Ignore policies that don't exist, since they don't grant any
|
||||
// more privilege.
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the policy to the tracking array.
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
|
||||
// Iterate all the token role links, so we can unpack these and identify
|
||||
// the ACL policies.
|
||||
for _, roleLink := range token.Roles {
|
||||
|
||||
// Any error reading the role means we cannot move forward. We just
|
||||
// ignore any roles that have been detailed but are not within our
|
||||
// state.
|
||||
role, err := snap.GetACLRoleByID(nil, roleLink.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unpack the policies held within the ACL role to form a single list
|
||||
// of ACL policies that this token has available.
|
||||
for _, policyLink := range role.Policies {
|
||||
policy, err := snap.ACLPolicyByName(nil, policyLink.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ignore policies that don't exist, since they don't grant any
|
||||
// more privilege.
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the policy to the tracking array.
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and cache the ACL object
|
||||
aclObj, err := structs.CompileACLObject(cache, policies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aclObj, nil
|
||||
}
|
||||
|
||||
// resolveSecretToken is used to translate an ACL Token Secret ID into a
|
||||
// Accessor ID, the anonymous accessor, or an error.
|
||||
func (s *Authenticator) resolveSecretToken(secretID string) (*structs.ACLToken, error) {
|
||||
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "accessorForSecretToken"}, time.Now())
|
||||
|
||||
if !s.aclsEnabled {
|
||||
return structs.ACLsDisabledToken, nil
|
||||
}
|
||||
if secretID == "" {
|
||||
return structs.AnonymousACLToken, nil
|
||||
}
|
||||
if !helper.IsUUID(secretID) {
|
||||
return nil, structs.ErrTokenInvalid
|
||||
}
|
||||
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Lookup the ACL Token
|
||||
token, err := snap.ACLTokenBySecretID(nil, secretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
}
|
||||
if token.IsExpired(time.Now().UTC()) {
|
||||
return nil, structs.ErrTokenExpired
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *Authenticator) ResolvePoliciesForClaims(claims *structs.IdentityClaims) ([]*structs.ACLPolicy, error) {
|
||||
|
||||
snap, err := s.getState().Snapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alloc, err := snap.AllocByID(nil, claims.AllocationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alloc == nil || alloc.Job == nil {
|
||||
return nil, fmt.Errorf("allocation does not exist")
|
||||
}
|
||||
|
||||
// Find any policies attached to the job
|
||||
jobId := alloc.Job.GetIDforWorkloadIdentity()
|
||||
iter, err := snap.ACLPolicyByJob(nil, alloc.Namespace, jobId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies := []*structs.ACLPolicy{}
|
||||
for {
|
||||
raw := iter.Next()
|
||||
if raw == nil {
|
||||
break
|
||||
}
|
||||
policy := raw.(*structs.ACLPolicy)
|
||||
if policy.JobACL == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case policy.JobACL.Group == "":
|
||||
policies = append(policies, policy)
|
||||
case policy.JobACL.Group != alloc.TaskGroup:
|
||||
continue // don't bother checking task
|
||||
case policy.JobACL.Task == "":
|
||||
policies = append(policies, policy)
|
||||
case policy.JobACL.Task == claims.TaskName:
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
iter, err = snap.ACLPolicyByNamespace(nil, alloc.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
raw := iter.Next()
|
||||
if raw == nil {
|
||||
break
|
||||
}
|
||||
|
||||
policy := raw.(*structs.ACLPolicy)
|
||||
if policy.JobACL == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user