auth: add client-only ACL (#18730)

The RPC handlers expect to see `nil` ACL objects whenever ACLs are disabled. By
using `nil` as a sentinel value, we have the risk of nil pointer exceptions and
improper handling of `nil` when returned from our various auth methods that can
lead to privilege escalation bugs. This is the third in a series to eliminate
the use of `nil` ACLs as a sentinel value for when ACLs are disabled.

This patch involves creating a new "virtual" ACL object for checking permissions
on client operations and a matching `AuthenticateClientOnly` method for
client-only RPCs that can produce that ACL.

Unlike the server ACLs PR, this also includes a special case for "legacy" client
RPCs where the client was not previously sending the secret as it
should (leaning on mTLS only). Those client RPCs were fixed in Nomad 1.6.0, but
it'll take a while before we can guarantee they'll be present during upgrades.

Ref: https://github.com/hashicorp/nomad-enterprise/pull/1218
Ref: https://github.com/hashicorp/nomad/pull/18703
Ref: https://github.com/hashicorp/nomad/pull/18715
Ref: https://github.com/hashicorp/nomad/pull/16799
This commit is contained in:
Tim Gross
2023-10-12 12:21:48 -04:00
committed by GitHub
parent cecd9b0472
commit 3633ca0f8c
25 changed files with 467 additions and 244 deletions

View File

@@ -40,17 +40,38 @@ rules:
}
...
# Pattern used by endpoints that are used by both ACLs and Clients.
# These endpoints will always have a ctx passed to Authenticate
# Pattern used by endpoints that are used only for client-to-server.
# Authorization can be done after forwarding, but must check the
# AllowClientOp policy
- pattern-not-inside: |
authErr := $A.$B.Authenticate($A.ctx, args)
aclObj, err := $A.$B.AuthenticateClientOnly($A.ctx, args)
...
if done, err := $A.$B.forward($METHOD, ...); done {
return err
}
...
... := $A.$B.ResolveClientOrACL(...)
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
...
# Pattern used by endpoints that are used only for client-to-server.
# Authorization can be done after forwarding, but must check the
# AllowClientOp policy. This should not be added to any new endpoints.
- pattern-not-inside: |
aclObj, err := $A.$B.AuthenticateClientOnlyLegacy($A.ctx, args)
...
if done, err := $A.$B.forward($METHOD, ...); done {
return err
}
...
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
...
# Pattern used by ACL endpoints that need to interact with the token directly
- pattern-not-inside: |
authErr := $A.$B.Authenticate($A.ctx, args)

View File

@@ -78,6 +78,7 @@ type ACL struct {
// The attributes below detail a virtual policy that we never expose
// directly to the end user.
client string
server string
isLeader bool
}
@@ -301,6 +302,7 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
acl.variables = svTxn.Commit()
acl.wildcardVariables = wsvTxn.Commit()
acl.client = PolicyDeny
acl.server = PolicyDeny
acl.isLeader = false
@@ -332,6 +334,11 @@ func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
return true
}
// Clients need to be able to read their namespaced objects
if a.client != PolicyDeny {
return true
}
// If using the all namespaces wildcard, allow if any namespace allows the
// operation.
if ns == AllNamespacesSentinel && a.anyNamespaceAllowsOp(op) {
@@ -731,6 +738,9 @@ func (a *ACL) AllowNodeRead() bool {
return true
case a.node == PolicyRead:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.server == PolicyRead,
a.server == PolicyWrite:
return true
@@ -813,6 +823,9 @@ func (a *ACL) AllowPluginRead() bool {
return true
case a.management:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.plugin == PolicyRead:
return true
default:
@@ -828,6 +841,9 @@ func (a *ACL) AllowPluginList() bool {
return true
case a.management:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.plugin == PolicyList:
return true
case a.plugin == PolicyRead:
@@ -837,6 +853,15 @@ func (a *ACL) AllowPluginList() bool {
}
}
func (a *ACL) AllowServiceRegistrationReadList(ns string, isWorkload bool) bool {
if a == nil {
// ACL is nil only if ACLs are disabled
// TODO(tgross): return false when there are no nil ACLs
return true
}
return isWorkload || a.AllowNsOp(ns, NamespaceCapabilityReadJob)
}
// AllowServerOp checks if server-only operations are allowed
func (a *ACL) AllowServerOp() bool {
if a == nil {
@@ -847,6 +872,15 @@ func (a *ACL) AllowServerOp() bool {
return a.server != PolicyDeny || a.isLeader
}
func (a *ACL) AllowClientOp() bool {
if a == nil {
// ACL is nil only if ACLs are disabled
// TODO(tgross): return false when there are no nil ACLs
return true
}
return a.client != PolicyDeny
}
// IsManagement checks if this represents a management token
func (a *ACL) IsManagement() bool {
return a.management
@@ -856,14 +890,18 @@ func (a *ACL) IsManagement() bool {
// a list of operations. Returns true (allowed) if acls are disabled or if
// *any* capabilities match.
func NamespaceValidator(ops ...string) func(*ACL, string) bool {
return func(acl *ACL, ns string) bool {
return func(a *ACL, ns string) bool {
// Always allow if ACLs are disabled.
if acl == nil {
if a == nil {
return true
}
// Clients need to be able to read namespaced objects
if a.client != PolicyDeny {
return true
}
for _, op := range ops {
if acl.AllowNamespaceOperation(ns, op) {
if a.AllowNamespaceOperation(ns, op) {
// An operation is allowed, return true
return true
}

View File

@@ -100,6 +100,7 @@ func TestACLManagement(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.True(t, acl.AllowQuotaWrite())
must.True(t, acl.AllowServerOp())
must.True(t, acl.AllowClientOp())
}
func TestACLMerge(t *testing.T) {
@@ -143,6 +144,7 @@ func TestACLMerge(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.True(t, acl.AllowQuotaWrite())
must.False(t, acl.AllowServerOp())
must.False(t, acl.AllowClientOp())
// Merge read + blank
p3, err := Parse("")
@@ -178,6 +180,7 @@ func TestACLMerge(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.False(t, acl.AllowQuotaWrite())
must.False(t, acl.AllowServerOp())
must.False(t, acl.AllowClientOp())
// Merge read + deny
p4, err := Parse(denyAll)

View File

@@ -3,8 +3,20 @@
package acl
var ClientACL = initClientACL()
var ServerACL = initServerACL()
func initClientACL() *ACL {
aclObj, err := NewACL(false, []*Policy{})
if err != nil {
panic(err)
}
aclObj.client = PolicyWrite
aclObj.agent = PolicyRead
aclObj.server = PolicyRead
return aclObj
}
func initServerACL() *ACL {
aclObj, err := NewACL(false, []*Policy{})
if err != nil {

View File

@@ -30,7 +30,7 @@ func TestWIDMgr(t *testing.T) {
t.Cleanup(ta.Shutdown)
mgr := widmgr.NewSigner(widmgr.SignerConfig{
NodeSecret: uuid.Generate(), // not checked when ACLs disabled
NodeSecret: ta.Client().Node().SecretID,
Region: "global",
RPC: ta,
})

View File

@@ -212,7 +212,7 @@ func TestHTTP_Alloc_Port_Response(t *testing.T) {
ci.Parallel(t)
httpTest(t, nil, func(srv *TestAgent) {
client := srv.Client()
client := srv.APIClient()
defer srv.Shutdown()
defer client.Close()

View File

@@ -31,6 +31,6 @@ func testServer(t *testing.T, runClient bool, cb func(*Config)) (*TestAgent, *ap
})
t.Cleanup(a.Shutdown)
c := a.Client()
c := a.APIClient()
return a, c, a.HTTPAddr()
}

View File

@@ -309,7 +309,7 @@ func (a *TestAgent) HTTPAddr() string {
return proto + a.Server.Addr
}
func (a *TestAgent) Client() *api.Client {
func (a *TestAgent) APIClient() *api.Client {
conf := api.DefaultConfig()
conf.Address = a.HTTPAddr()
c, err := api.NewClient(conf)

View File

@@ -898,7 +898,7 @@ func testServerWithoutLeader(t *testing.T, runClient bool, cb func(*agent.Config
})
t.Cleanup(func() { a.Shutdown() })
c := a.Client()
c := a.APIClient()
return a, c, a.HTTPAddr()
}

View File

@@ -22,7 +22,7 @@ func TestOperatorSchedulerSetConfig_Run(t *testing.T) {
ui := cli.NewMockUi()
c := &OperatorSchedulerSetConfig{Meta: Meta{Ui: ui}}
bootstrappedConfig, _, err := srv.Client().Operator().SchedulerGetConfiguration(nil)
bootstrappedConfig, _, err := srv.APIClient().Operator().SchedulerGetConfiguration(nil)
require.NoError(t, err)
require.NotEmpty(t, bootstrappedConfig.SchedulerConfig)
@@ -34,7 +34,7 @@ func TestOperatorSchedulerSetConfig_Run(t *testing.T) {
// Read the configuration again and test that nothing has changed which
// ensures our empty flags are working correctly.
nonModifiedConfig, _, err := srv.Client().Operator().SchedulerGetConfiguration(nil)
nonModifiedConfig, _, err := srv.APIClient().Operator().SchedulerGetConfiguration(nil)
require.NoError(t, err)
schedulerConfigEquals(t, bootstrappedConfig.SchedulerConfig, nonModifiedConfig.SchedulerConfig)
@@ -56,7 +56,7 @@ func TestOperatorSchedulerSetConfig_Run(t *testing.T) {
s := ui.OutputWriter.String()
require.Contains(t, s, "Scheduler configuration updated!")
modifiedConfig, _, err := srv.Client().Operator().SchedulerGetConfiguration(nil)
modifiedConfig, _, err := srv.APIClient().Operator().SchedulerGetConfiguration(nil)
require.NoError(t, err)
schedulerConfigEquals(t, &api.SchedulerConfiguration{
SchedulerAlgorithm: "spread",

View File

@@ -31,7 +31,7 @@ func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.Te
})
t.Cleanup(a.Shutdown)
c := a.Client()
c := a.APIClient()
return a, c, a.HTTPAddr()
}
@@ -46,7 +46,7 @@ func testClient(t *testing.T, name string, cb func(*agent.Config)) (*agent.TestA
})
t.Cleanup(a.Shutdown)
c := a.Client()
c := a.APIClient()
t.Logf("Waiting for client %s to join server(s) %s", name, a.GetConfig().Client.Servers)
testutil.WaitForClient(t, a.Agent.RPC, a.Agent.Client().NodeID(), a.Agent.Client().Region())

View File

@@ -101,7 +101,7 @@ func TestVarLockCommand_Good(t *testing.T) {
code := cmd.Run([]string{"-address=" + url, "test/var/shell", "touch ", filePath})
require.Equal(t, 0, code, "expected exit 0, got: %d; %v", code, ui.ErrorWriter.String())
sv, _, err := srv.Client().Variables().Peek("test/var/shell", nil)
sv, _, err := srv.APIClient().Variables().Peek("test/var/shell", nil)
must.NoError(t, err)
must.NotNil(t, sv)
@@ -135,7 +135,7 @@ func TestVarLockCommand_Good_NoShell(t *testing.T) {
code := cmd.Run([]string{"-address=" + url, "-shell=false", "test/var/noShell", "touch", filePath})
require.Zero(t, 0, code)
sv, _, err := srv.Client().Variables().Peek("test/var/noShell", nil)
sv, _, err := srv.APIClient().Variables().Peek("test/var/noShell", nil)
must.NoError(t, err)
must.NotNil(t, sv)

View File

@@ -16,6 +16,14 @@ func (srv *Server) AuthenticateServerOnly(ctx *RPCContext, args structs.RequestW
return srv.auth.AuthenticateServerOnly(ctx, args)
}
func (srv *Server) AuthenticateClientOnly(ctx *RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
return srv.auth.AuthenticateClientOnly(ctx, args)
}
func (srv *Server) AuthenticateClientOnlyLegacy(ctx *RPCContext, args structs.RequestWithIdentity) (*acl.ACL, error) {
return srv.auth.AuthenticateClientOnlyLegacy(ctx, args)
}
func (srv *Server) ResolveACL(args structs.RequestWithIdentity) (*acl.ACL, error) {
return srv.auth.ResolveACL(args)
}
@@ -28,10 +36,6 @@ func (srv *Server) ResolveToken(secretID string) (*acl.ACL, error) {
return srv.auth.ResolveToken(secretID)
}
func (srv *Server) ResolveClientOrACL(args structs.RequestWithIdentity) (*acl.ACL, error) {
return srv.auth.ResolveClientOrACL(args)
}
func (srv *Server) ResolvePoliciesForClaims(claims *structs.IdentityClaims) ([]*structs.ACLPolicy, error) {
return srv.auth.ResolvePoliciesForClaims(claims)
}

View File

@@ -155,7 +155,7 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest,
// Check namespace read-job permissions before performing blocking query.
allowNsOp := acl.NamespaceValidator(acl.NamespaceCapabilityReadJob)
aclObj, err := a.srv.ResolveClientOrACL(args)
aclObj, err := a.srv.ResolveACL(args)
if err != nil {
return err
}
@@ -200,21 +200,20 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest,
func (a *Alloc) GetAllocs(args *structs.AllocsGetRequest,
reply *structs.AllocsGetResponse) error {
authErr := a.srv.Authenticate(a.ctx, args)
// Ensure the connection was initiated by a client if TLS is used.
err := validateTLSCertificateLevel(a.srv, a.ctx, tlsCertificateLevelClient)
aclObj, err := a.srv.AuthenticateClientOnly(a.ctx, args)
a.srv.MeasureRPCRate("alloc", structs.RateMetricWrite, args)
if err != nil {
return err
return structs.ErrPermissionDenied
}
if done, err := a.srv.forward("Alloc.GetAllocs", args, args, reply); done {
return err
}
a.srv.MeasureRPCRate("alloc", structs.RateMetricList, args)
if authErr != nil {
defer metrics.MeasureSince([]string{"nomad", "alloc", "get_allocs"}, time.Now())
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
defer metrics.MeasureSince([]string{"nomad", "alloc", "get_allocs"}, time.Now())
allocs := make([]*structs.Allocation, len(args.AllocIDs))
@@ -452,22 +451,21 @@ func (a *Alloc) GetServiceRegistrations(
// This is an internal-only RPC and not exposed via the HTTP API.
func (a *Alloc) SignIdentities(args *structs.AllocIdentitiesRequest, reply *structs.AllocIdentitiesResponse) error {
authErr := a.srv.Authenticate(a.ctx, args)
// Ensure the connection was initiated by a client if TLS is used.
if err := validateTLSCertificateLevel(a.srv, a.ctx, tlsCertificateLevelClient); err != nil {
return err
}
if done, err := a.srv.forward("Alloc.SignIdentities", args, args, reply); done {
return err
}
aclObj, err := a.srv.AuthenticateClientOnly(a.ctx, args)
a.srv.MeasureRPCRate("alloc", structs.RateMetricRead, args)
if authErr != nil {
if err != nil {
return structs.ErrPermissionDenied
}
if done, err := a.srv.forward("Alloc.SignIdentities", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "alloc", "sign_identities"}, time.Now())
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
if len(args.Identities) == 0 {
// Client bug. Fail loudly instead of letting clients waste time with
// noops.

View File

@@ -948,11 +948,15 @@ func TestAllocEndpoint_GetAllocs(t *testing.T) {
t.Fatalf("err: %v", err)
}
node := mock.Node()
state.UpsertNode(structs.MsgTypeTestSetup, 1001, node)
// Lookup the allocs
get := &structs.AllocsGetRequest{
AllocIDs: []string{alloc.ID, alloc2.ID},
QueryOptions: structs.QueryOptions{
Region: "global",
Region: "global",
AuthToken: node.SecretID,
},
}
var resp structs.AllocsGetResponse
@@ -986,6 +990,9 @@ func TestAllocEndpoint_GetAllocs_Blocking(t *testing.T) {
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
node := mock.Node()
state.UpsertNode(structs.MsgTypeTestSetup, 50, node)
// Create the allocs
alloc1 := mock.Alloc()
alloc2 := mock.Alloc()
@@ -1014,6 +1021,7 @@ func TestAllocEndpoint_GetAllocs_Blocking(t *testing.T) {
QueryOptions: structs.QueryOptions{
Region: "global",
MinQueryIndex: 150,
AuthToken: node.SecretID,
},
}
var resp structs.AllocsGetResponse
@@ -1684,11 +1692,15 @@ func TestAlloc_SignIdentities_Bad(t *testing.T) {
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
node := mock.Node()
must.NoError(t, s1.fsm.State().UpsertNode(structs.MsgTypeTestSetup, 100, node))
req := &structs.AllocIdentitiesRequest{
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: structs.DefaultNamespace,
AllowStale: true,
AuthToken: node.SecretID,
},
}
var resp structs.AllocIdentitiesResponse
@@ -1771,6 +1783,9 @@ func TestAlloc_SignIdentities_Blocking(t *testing.T) {
testutil.WaitForLeader(t, s1.RPC)
state := s1.fsm.State()
node := mock.Node()
must.NoError(t, s1.fsm.State().UpsertNode(structs.MsgTypeTestSetup, 100, node))
// Create the alloc we're going to query for, but don't insert it yet. This
// simulates querying a slow follower or a restoring server.
alloc := mock.Alloc()
@@ -1811,6 +1826,7 @@ func TestAlloc_SignIdentities_Blocking(t *testing.T) {
AllowStale: true,
MinQueryIndex: 1999,
MaxQueryTime: 10 * time.Second,
AuthToken: node.SecretID,
},
}
var resp structs.AllocIdentitiesResponse

View File

@@ -17,6 +17,7 @@ import (
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
"golang.org/x/exp/slices"
)
// aclCacheSize is the number of ACL objects to keep cached. ACLs have a parsing
@@ -46,6 +47,9 @@ type Authenticator struct {
getLeaderACL LeaderACLGetter
region string
validServerCertNames []string
validClientCertNames []string
// aclCache is used to maintain the parsed ACL objects
aclCache *structs.ACLCache[*acl.ACL]
@@ -66,14 +70,19 @@ type AuthenticatorConfig struct {
func NewAuthenticator(cfg *AuthenticatorConfig) *Authenticator {
return &Authenticator{
aclsEnabled: cfg.AclsEnabled,
tlsEnabled: cfg.TLSEnabled,
logger: cfg.Logger.With("auth"),
getState: cfg.StateFn,
getLeaderACL: cfg.GetLeaderACLFn,
region: cfg.Region,
aclCache: structs.NewACLCache[*acl.ACL](aclCacheSize),
encrypter: cfg.Encrypter,
aclsEnabled: cfg.AclsEnabled,
tlsEnabled: cfg.TLSEnabled,
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",
},
}
}
@@ -232,9 +241,7 @@ func (s *Authenticator) AuthenticateServerOnly(ctx RPCContext, args structs.Requ
// set on the identity whether or not its valid for server RPC, so we
// can capture it for metrics
identity.TLSName = tlsCert.Subject.CommonName
expected := "server." + s.region + ".nomad"
_, err := validateCertificateForName(tlsCert, expected)
_, err := validateCertificateForNames(tlsCert, s.validServerCertNames)
if err != nil {
return nil, err
}
@@ -249,23 +256,111 @@ func (s *Authenticator) AuthenticateServerOnly(ctx RPCContext, args structs.Requ
return acl.ServerACL, nil
}
// validateCertificateForName returns true if the certificate is valid
// for the given domain name.
func validateCertificateForName(cert *x509.Certificate, expectedName string) (bool, error) {
// 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, which should use AuthenticateClientTOFU
//
// The returned ACL object is always a acl.ClientACL but in the future this
// could be extended to allow clients access only to their own pool and
// associated namespaces, etc.
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 s.tlsEnabled && !ctx.IsStatic() {
tlsCert := ctx.Certificate()
if tlsCert == nil {
return nil, errors.New("missing certificate information")
}
// set on the identity whether or not its valid for server RPC, so we
// can capture it for metrics
identity.TLSName = tlsCert.Subject.CommonName
_, err := validateCertificateForNames(tlsCert, s.validClientCertNames)
if err != nil {
return nil, err
}
}
secretID := args.GetAuthToken()
if secretID == "" {
return nil, structs.ErrPermissionDenied
}
// 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 nil, fmt.Errorf("could not resolve node secret: %w", err)
}
if node == nil {
return nil, structs.ErrPermissionDenied
}
identity.ClientID = node.ID
return acl.ClientACL, nil
}
// AuthenticateClientOnlyLegacy is a version of AuthenticateClientOnly that's
// used by a few older RPCs that did not properly enforce node secrets.
// COMPAT(1.8.0): In Nomad 1.6.0 we starting sending those node secrets, so we
// can remove this in Nomad 1.8.0.
func (s *Authenticator) AuthenticateClientOnlyLegacy(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 s.tlsEnabled && !ctx.IsStatic() {
tlsCert := ctx.Certificate()
if tlsCert == nil {
return nil, errors.New("missing certificate information")
}
// set on the identity whether or not its valid for server RPC, so we
// can capture it for metrics
identity.TLSName = tlsCert.Subject.CommonName
_, err := validateCertificateForNames(tlsCert, s.validClientCertNames)
if err != nil {
return nil, err
}
}
return acl.ClientACL, 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 _, valid := range validNames {
if expectedName == valid {
for _, expectedName := range expectedNames {
if slices.Contains(validNames, expectedName) {
return true, nil
}
}
return false, fmt.Errorf("invalid certificate, %s not in %s",
expectedName, strings.Join(validNames, ","))
return false, fmt.Errorf("invalid certificate: %s not in expected %s",
strings.Join(validNames, ", "),
strings.Join(expectedNames, ", "))
}
// ResolveACLForToken resolves an ACL from a token only. It should be used only
@@ -285,22 +380,6 @@ func (s *Authenticator) ResolveACLForToken(aclToken *structs.ACLToken) (*acl.ACL
return resolveACLFromToken(snap, s.aclCache, aclToken)
}
// ResolveClientOrACL resolves an ACL if the identity has a token or claim, and
// falls back to verifying the client ID if one has been set
func (s *Authenticator) ResolveClientOrACL(args structs.RequestWithIdentity) (*acl.ACL, error) {
identity := args.GetIdentity()
if !s.aclsEnabled || identity == nil || identity.ClientID != "" {
return nil, nil
}
aclObj, err := s.ResolveACL(args)
if err != nil {
return nil, err
}
// Returns either the users aclObj, or nil if ACLs are disabled.
return aclObj, nil
}
// ResolveToken is used to translate an ACL Token Secret ID into
// an ACL object, nil if ACLs are disabled, or an error.
func (s *Authenticator) ResolveToken(secretID string) (*acl.ACL, error) {

View File

@@ -383,7 +383,7 @@ func TestAuthenticateServerOnly(t *testing.T) {
aclObj, err := auth.AuthenticateServerOnly(ctx, args)
must.EqError(t, err,
"invalid certificate, server.global.nomad not in client.global.nomad")
"invalid certificate: client.global.nomad not in expected server.global.nomad")
must.Eq(t, "client.global.nomad:192.168.1.1", args.GetIdentity().String())
must.Nil(t, aclObj)
},
@@ -414,6 +414,165 @@ func TestAuthenticateServerOnly(t *testing.T) {
}
func TestAuthenticateClientOnly(t *testing.T) {
ci.Parallel(t)
testAuthenticator := func(t *testing.T, store *state.StateStore,
hasACLs, hasTLS bool) *Authenticator {
leaderACL := uuid.Generate()
return NewAuthenticator(&AuthenticatorConfig{
StateFn: func() *state.StateStore { return store },
Logger: testlog.HCLogger(t),
GetLeaderACLFn: func() string { return leaderACL },
AclsEnabled: hasACLs,
TLSEnabled: hasTLS,
Region: "global",
Encrypter: nil,
})
}
testCases := []struct {
name string
testFn func(*testing.T, *state.StateStore, *structs.Node)
}{
{
name: "no mTLS or ACLs but no node secret",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, noTLSCtx, "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = ""
auth := testAuthenticator(t, store, false, false)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.ErrorIs(t, err, structs.ErrPermissionDenied)
must.Eq(t, ":192.168.1.1", args.GetIdentity().String())
must.Nil(t, aclObj)
},
},
{
name: "no mTLS or ACLs but with node secret",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, noTLSCtx, "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = node.SecretID
auth := testAuthenticator(t, store, false, false)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.NoError(t, err)
must.NotNil(t, aclObj)
must.Eq(t, "client:"+node.ID, args.GetIdentity().String())
},
},
{
name: "no mTLS but with ACLs",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, noTLSCtx, "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = node.SecretID
auth := testAuthenticator(t, store, true, false)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.NoError(t, err)
must.NotNil(t, aclObj)
must.Eq(t, "client:"+node.ID, args.GetIdentity().String())
must.True(t, aclObj.AllowClientOp())
},
},
{
name: "no mTLS but with ACLs and bad secret",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, noTLSCtx, "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = uuid.Generate()
auth := testAuthenticator(t, store, true, false)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.ErrorIs(t, err, structs.ErrPermissionDenied)
must.Eq(t, ":192.168.1.1", args.GetIdentity().String())
must.Nil(t, aclObj)
},
},
{
name: "with mTLS and ACLs but CLI cert",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, "cli.global.nomad", "192.168.1.1")
args := &structs.GenericRequest{}
auth := testAuthenticator(t, store, true, true)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.EqError(t, err,
"invalid certificate: cli.global.nomad not in expected client.global.nomad, server.global.nomad")
must.Eq(t, "cli.global.nomad:192.168.1.1", args.GetIdentity().String())
must.Nil(t, aclObj)
},
},
{
name: "with mTLS and ACLs with server cert but bad token",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, "server.global.nomad", "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = uuid.Generate()
auth := testAuthenticator(t, store, true, true)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.ErrorIs(t, err, structs.ErrPermissionDenied)
must.Eq(t, "server.global.nomad:192.168.1.1", args.GetIdentity().String())
must.Nil(t, aclObj)
},
},
{
name: "with mTLS and ACLs with server cert and valid token",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, "server.global.nomad", "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = node.SecretID
auth := testAuthenticator(t, store, true, true)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.NoError(t, err)
must.Eq(t, "client:"+node.ID, args.GetIdentity().String())
must.NotNil(t, aclObj)
must.True(t, aclObj.AllowClientOp())
},
},
{
name: "with mTLS and ACLs with client cert",
testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) {
ctx := newTestContext(t, "client.global.nomad", "192.168.1.1")
args := &structs.GenericRequest{}
args.AuthToken = node.SecretID
auth := testAuthenticator(t, store, true, true)
aclObj, err := auth.AuthenticateClientOnly(ctx, args)
must.NoError(t, err)
must.Eq(t, "client:"+node.ID, args.GetIdentity().String())
must.NotNil(t, aclObj)
must.True(t, aclObj.AllowClientOp())
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
node := mock.Node()
store := testStateStore(t)
store.UpsertNode(structs.MsgTypeTestSetup, 100, node)
tc.testFn(t, store, node)
})
}
}
func TestResolveACLToken(t *testing.T) {
ci.Parallel(t)

View File

@@ -189,7 +189,7 @@ func (v *CSIVolume) Get(args *structs.CSIVolumeGetRequest, reply *structs.CSIVol
allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIReadVolume,
acl.NamespaceCapabilityCSIMountVolume,
acl.NamespaceCapabilityReadJob)
aclObj, err := v.srv.ResolveClientOrACL(args)
aclObj, err := v.srv.ResolveACL(args)
if err != nil {
return err
}
@@ -452,14 +452,13 @@ func (v *CSIVolume) Claim(args *structs.CSIVolumeClaimRequest, reply *structs.CS
return structs.ErrPermissionDenied
}
defer metrics.MeasureSince([]string{"nomad", "volume", "claim"}, time.Now())
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIMountVolume)
aclObj, err := v.srv.ResolveClientOrACL(args)
aclObj, err := v.srv.ResolveACL(args)
if err != nil {
return err
}
defer metrics.MeasureSince([]string{"nomad", "volume", "claim"}, time.Now())
if !allowVolume(aclObj, args.RequestNamespace()) || !aclObj.AllowPluginRead() {
return structs.ErrPermissionDenied
}
@@ -694,7 +693,7 @@ func (v *CSIVolume) Unpublish(args *structs.CSIVolumeUnpublishRequest, reply *st
defer metrics.MeasureSince([]string{"nomad", "volume", "unpublish"}, time.Now())
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIMountVolume)
aclObj, err := v.srv.ResolveClientOrACL(args)
aclObj, err := v.srv.ResolveACL(args)
if err != nil {
return err
}

View File

@@ -1021,7 +1021,7 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest,
defer metrics.MeasureSince([]string{"nomad", "client", "get_node"}, time.Now())
// Check node read permissions
aclObj, err := n.srv.ResolveClientOrACL(args)
aclObj, err := n.srv.ResolveACL(args)
if err != nil {
return err
}
@@ -1317,23 +1317,21 @@ func (n *Node) GetClientAllocs(args *structs.NodeSpecificRequest,
// - The node status is down or disconnected. Clients must call the
// UpdateStatus method to update its status in the server.
func (n *Node) UpdateAlloc(args *structs.AllocUpdateRequest, reply *structs.GenericResponse) error {
authErr := n.srv.Authenticate(n.ctx, args)
// Ensure the connection was initiated by another client if TLS is used.
err := validateTLSCertificateLevel(n.srv, n.ctx, tlsCertificateLevelClient)
if err != nil {
return err
}
if done, err := n.srv.forward("Node.UpdateAlloc", args, args, reply); done {
return err
}
// COMPAT(1.9.0): move to AuthenticateClientOnly
aclObj, err := n.srv.AuthenticateClientOnlyLegacy(n.ctx, args)
n.srv.MeasureRPCRate("node", structs.RateMetricWrite, args)
if authErr != nil {
if err != nil {
return structs.ErrPermissionDenied
}
if done, err := n.srv.forward("Node.UpdateAlloc", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "client", "update_alloc"}, time.Now())
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
// Ensure at least a single alloc
if len(args.Alloc) == 0 {
@@ -2253,22 +2251,21 @@ func taskUsesConnect(task *structs.Task) bool {
}
func (n *Node) EmitEvents(args *structs.EmitNodeEventsRequest, reply *structs.EmitNodeEventsResponse) error {
authErr := n.srv.Authenticate(n.ctx, args)
// Ensure the connection was initiated by another client if TLS is used.
err := validateTLSCertificateLevel(n.srv, n.ctx, tlsCertificateLevelClient)
// COMPAT(1.9.0): move to AuthenticateClientOnly
aclObj, err := n.srv.AuthenticateClientOnlyLegacy(n.ctx, args)
n.srv.MeasureRPCRate("node", structs.RateMetricWrite, args)
if err != nil {
return err
return structs.ErrPermissionDenied
}
if done, err := n.srv.forward("Node.EmitEvents", args, args, reply); done {
return err
}
n.srv.MeasureRPCRate("node", structs.RateMetricWrite, args)
if authErr != nil {
defer metrics.MeasureSince([]string{"nomad", "client", "emit_events"}, time.Now())
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
defer metrics.MeasureSince([]string{"nomad", "client", "emit_events"}, time.Now())
if len(args.NodeEvents) == 0 {
return fmt.Errorf("no node events given")

View File

@@ -4514,8 +4514,11 @@ func TestClientEndpoint_UpdateAlloc_Evals_ByTrigger(t *testing.T) {
}
updateReq := &structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{clientAlloc},
WriteRequest: structs.WriteRequest{Region: "global"},
Alloc: []*structs.Allocation{clientAlloc},
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: node.SecretID,
},
}
var nodeAllocResp structs.NodeAllocsResponse

View File

@@ -487,7 +487,7 @@ func TestOperator_TransferLeadershipToServerAddress_ACL(t *testing.T) {
// Create ACL token
invalidToken := mock.CreatePolicyAndToken(t, state, 1001, "test-invalid", mock.NodePolicy(acl.PolicyWrite))
arg := structs.RaftPeerRequest{
arg := &structs.RaftPeerRequest{
RaftIDAddress: structs.RaftIDAddress{Address: addr},
WriteRequest: structs.WriteRequest{Region: s1.config.Region},
}
@@ -496,7 +496,7 @@ func TestOperator_TransferLeadershipToServerAddress_ACL(t *testing.T) {
t.Run("no-token", func(t *testing.T) {
// Try with no token and expect permission denied
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.Error(t, err)
must.ErrorIs(t, err, rpcPermDeniedErr)
})
@@ -504,7 +504,7 @@ func TestOperator_TransferLeadershipToServerAddress_ACL(t *testing.T) {
t.Run("invalid-token", func(t *testing.T) {
// Try with an invalid token and expect permission denied
arg.AuthToken = invalidToken.SecretID
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.Error(t, err)
must.ErrorIs(t, err, rpcPermDeniedErr)
})
@@ -512,7 +512,7 @@ func TestOperator_TransferLeadershipToServerAddress_ACL(t *testing.T) {
t.Run("good-token", func(t *testing.T) {
// Try with a management token
arg.AuthToken = tc.token.SecretID
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.NoError(t, err)
// Is the expected leader the new one?
@@ -543,7 +543,7 @@ func TestOperator_TransferLeadershipToServerID_ACL(t *testing.T) {
// Create ACL token
invalidToken := mock.CreatePolicyAndToken(t, state, 1001, "test-invalid", mock.NodePolicy(acl.PolicyWrite))
arg := structs.RaftPeerRequest{
arg := &structs.RaftPeerRequest{
RaftIDAddress: structs.RaftIDAddress{
ID: tgtID,
},
@@ -554,7 +554,7 @@ func TestOperator_TransferLeadershipToServerID_ACL(t *testing.T) {
t.Run("no-token", func(t *testing.T) {
// Try with no token and expect permission denied
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.Error(t, err)
must.ErrorIs(t, err, rpcPermDeniedErr)
})
@@ -562,7 +562,7 @@ func TestOperator_TransferLeadershipToServerID_ACL(t *testing.T) {
t.Run("invalid-token", func(t *testing.T) {
// Try with an invalid token and expect permission denied
arg.AuthToken = invalidToken.SecretID
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.Error(t, err)
must.ErrorIs(t, err, rpcPermDeniedErr)
})
@@ -570,7 +570,7 @@ func TestOperator_TransferLeadershipToServerID_ACL(t *testing.T) {
t.Run("good-token", func(t *testing.T) {
// Try with a management token
arg.AuthToken = tc.token.SecretID
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", &arg, &reply)
err := msgpackrpc.CallWithCodec(codec, "Operator.TransferLeadershipToPeer", arg, &reply)
must.NoError(t, err)
// Is the expected leader the new one?

View File

@@ -1257,7 +1257,7 @@ func TestRPC_TLS_Enforcement_RPC(t *testing.T) {
name: "other region server/clients only rpc",
cn: "server.other.nomad",
rpcs: localClientsOnlyRPCs,
expectErr: "(certificate|broken pipe)",
expectErr: "(Permission denied|broken pipe)",
},
// Other region client.
{

View File

@@ -40,20 +40,20 @@ func (s *ServiceRegistration) Upsert(
args *structs.ServiceRegistrationUpsertRequest,
reply *structs.ServiceRegistrationUpsertResponse) error {
authErr := s.srv.Authenticate(s.ctx, args)
// Ensure the connection was initiated by a client if TLS is used.
if err := validateTLSCertificateLevel(s.srv, s.ctx, tlsCertificateLevelClient); err != nil {
return err
aclObj, err := s.srv.AuthenticateClientOnly(s.ctx, args)
s.srv.MeasureRPCRate("service_registration", structs.RateMetricWrite, args)
if err != nil {
return structs.ErrPermissionDenied
}
if done, err := s.srv.forward(structs.ServiceRegistrationUpsertRPCMethod, args, args, reply); done {
return err
}
s.srv.MeasureRPCRate("service_registration", structs.RateMetricWrite, args)
if authErr != nil {
defer metrics.MeasureSince([]string{"nomad", "service_registration", "upsert"}, time.Now())
if !aclObj.AllowClientOp() {
return structs.ErrPermissionDenied
}
defer metrics.MeasureSince([]string{"nomad", "service_registration", "upsert"}, time.Now())
// Nomad service registrations can only be used once all servers, in the
// local region, have been upgraded to 1.3.0 or greater.
@@ -62,17 +62,6 @@ func (s *ServiceRegistration) Upsert(
minNomadServiceRegistrationVersion)
}
// This endpoint is only callable by nodes in the cluster. Therefore,
// perform a node lookup using the secret ID to confirm the caller is a
// known node.
node, err := s.srv.fsm.State().NodeBySecretID(nil, args.AuthToken)
if err != nil {
return err
}
if node == nil {
return structs.ErrTokenNotFound
}
// Use a multierror, so we can capture all validation errors and pass this
// back so fixing in a single swoop.
var mErr multierror.Error
@@ -124,41 +113,10 @@ func (s *ServiceRegistration) DeleteByID(
minNomadServiceRegistrationVersion)
}
// Perform the ACL token resolution.
aclObj, err := s.srv.ResolveACL(args)
switch err {
case nil:
// If ACLs are enabled, ensure the caller has the submit-job namespace
// capability.
if aclObj != nil {
hasSubmitJob := aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilitySubmitJob)
if !hasSubmitJob {
return structs.ErrPermissionDenied
}
}
default:
// This endpoint is generally called by Nomad nodes, so we want to
// perform this check, unless the token resolution gave us a terminal
// error.
if err != structs.ErrTokenNotFound {
return err
}
// Attempt to lookup AuthToken as a Node.SecretID and return any error
// wrapped along with the original.
node, stateErr := s.srv.fsm.State().NodeBySecretID(nil, args.AuthToken)
if stateErr != nil {
var mErr multierror.Error
mErr.Errors = append(mErr.Errors, err, stateErr)
return mErr.ErrorOrNil()
}
// At this point, we do not have a valid ACL token, nor are we being
// called, or able to confirm via the state store, by a node.
if node == nil {
return structs.ErrTokenNotFound
}
if aclObj, err := s.srv.ResolveACL(args); err != nil {
return structs.ErrPermissionDenied
} else if !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilitySubmitJob) {
return structs.ErrPermissionDenied
}
// Update via Raft.
@@ -216,12 +174,12 @@ func (s *ServiceRegistration) List(
return s.listAllServiceRegistrations(args, reply)
}
aclObj, err := s.srv.ResolveClientOrACL(args)
aclObj, err := s.srv.ResolveACL(args)
if err != nil {
return structs.ErrPermissionDenied
return err
}
if args.GetIdentity().Claims == nil &&
!aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) {
if !aclObj.AllowServiceRegistrationReadList(args.RequestNamespace(),
args.GetIdentity().Claims != nil) {
return structs.ErrPermissionDenied
}
@@ -381,12 +339,12 @@ func (s *ServiceRegistration) GetService(
}
defer metrics.MeasureSince([]string{"nomad", "service_registration", "get_service"}, time.Now())
aclObj, err := s.srv.ResolveClientOrACL(args)
aclObj, err := s.srv.ResolveACL(args)
if err != nil {
return structs.ErrPermissionDenied
}
if args.GetIdentity().Claims == nil &&
!aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) {
if !aclObj.AllowServiceRegistrationReadList(args.RequestNamespace(),
args.GetIdentity().Claims != nil) {
return structs.ErrPermissionDenied
}

View File

@@ -52,8 +52,7 @@ func TestServiceRegistration_Upsert(t *testing.T) {
var serviceRegResp structs.ServiceRegistrationUpsertResponse
err := msgpackrpc.CallWithCodec(
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
require.Error(t, err)
require.Contains(t, err.Error(), "node lookup by SecretID failed")
must.EqError(t, err, structs.ErrPermissionDenied.Error())
// Generate a node and retry the upsert.
node := mock.Node()
@@ -135,8 +134,7 @@ func TestServiceRegistration_Upsert(t *testing.T) {
var serviceRegResp structs.ServiceRegistrationUpsertResponse
err := msgpackrpc.CallWithCodec(
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
require.Error(t, err)
require.Contains(t, err.Error(), "node lookup by SecretID failed")
must.EqError(t, err, structs.ErrPermissionDenied.Error())
// Generate a node and retry the upsert.
node := mock.Node()

View File

@@ -282,65 +282,3 @@ func getAlloc(state AllocGetter, allocID string) (*structs.Allocation, error) {
return alloc, nil
}
// tlsCertificateLevel represents a role level for mTLS certificates.
type tlsCertificateLevel int8
const (
tlsCertificateLevelServer tlsCertificateLevel = iota
tlsCertificateLevelClient
)
// validateTLSCertificateLevel checks if the provided RPC connection was
// initiated with a certificate that matches the given TLS role level.
//
// - tlsCertificateLevelServer requires a server certificate.
// - tlsCertificateLevelServer requires a client or server certificate.
func validateTLSCertificateLevel(srv *Server, ctx *RPCContext, lvl tlsCertificateLevel) error {
switch lvl {
case tlsCertificateLevelClient:
err := validateLocalClientTLSCertificate(srv, ctx)
if err != nil {
return validateLocalServerTLSCertificate(srv, ctx)
}
return nil
case tlsCertificateLevelServer:
return validateLocalServerTLSCertificate(srv, ctx)
}
return fmt.Errorf("invalid TLS certificate level %v", lvl)
}
// validateLocalClientTLSCertificate checks if the provided RPC connection was
// initiated by a client in the same region as the target server.
func validateLocalClientTLSCertificate(srv *Server, ctx *RPCContext) error {
expected := fmt.Sprintf("client.%s.nomad", srv.Region())
err := validateTLSCertificate(srv, ctx, expected)
if err != nil {
return fmt.Errorf("invalid client connection in region %s: %v", srv.Region(), err)
}
return nil
}
// validateLocalServerTLSCertificate checks if the provided RPC connection was
// initiated by a server in the same region as the target server.
func validateLocalServerTLSCertificate(srv *Server, ctx *RPCContext) error {
expected := fmt.Sprintf("server.%s.nomad", srv.Region())
err := validateTLSCertificate(srv, ctx, expected)
if err != nil {
return fmt.Errorf("invalid server connection in region %s: %v", srv.Region(), err)
}
return nil
}
// validateTLSCertificate checks if the RPC connection mTLS certificates are
// valid for the given name.
func validateTLSCertificate(srv *Server, ctx *RPCContext, name string) error {
if srv.config.TLSConfig == nil || !srv.config.TLSConfig.VerifyServerHostname {
return nil
}
return ctx.ValidateCertificateForName(name)
}