From 4393c0e76ba76bae2dca81ddfb2621bf69eb10f1 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Fri, 27 Jun 2025 14:59:23 +0100 Subject: [PATCH] auth: Add authentication support for node identities. (#26148) The authenticator process which performs RPC authentication has been modified to support node identities. Node identities are verified by ensuring the node ID as claimed has a node written to Nomad state. The client only and generic authenticate methods now support both node secret IDs and node identities. It uses uuid checking to attempt to parse either option. A new method has also been added to handle the specific RPCs that will optionally generate node identities. While a new authenticator method is not ideal, it is better than the alternative option for these RPCs to perform complex additional RPC context work in order to understand whether an identity should be generated. The TLS verification functionality has been pulled into its own method to avoid further code duplication. --- nomad/auth/auth.go | 227 ++++++++++++++++----- nomad/auth/auth_test.go | 411 ++++++++++++++++++++++++++++++++++++++- nomad/structs/structs.go | 5 +- 3 files changed, 586 insertions(+), 57 deletions(-) diff --git a/nomad/auth/auth.go b/nomad/auth/auth.go index 9435a2e61..1e412961c 100644 --- a/nomad/auth/auth.go +++ b/nomad/auth/auth.go @@ -268,35 +268,86 @@ func (s *Authenticator) AuthenticateServerOnly(ctx RPCContext, args structs.Requ identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP} defer args.SetIdentity(identity) // always set the identity, even on errors - if s.verifyTLS.Load() && !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.validServerCertNames) - if err != nil { - return nil, err - } - return acl.ServerACL, nil - } - // 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/concepts/security#requirements + 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() { + 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, which should use AuthenticateClientTOFU +// with Node.Register or NodeUpdateStatus, which should use +// AuthenticateNodeIdentityGenerator. // // 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 @@ -311,40 +362,70 @@ func (s *Authenticator) AuthenticateClientOnly(ctx RPCContext, args structs.Requ identity := &structs.AuthenticatedIdentity{RemoteIP: remoteIP} defer args.SetIdentity(identity) // always set the identity, even on errors - if s.verifyTLS.Load() && !ctx.IsStatic() { - tlsCert := ctx.Certificate() - if tlsCert == nil { - return nil, errors.New("missing certificate information") - } + if err := verifyTLS(s.verifyTLS.Load(), ctx, s.validClientCertNames, identity); err != nil { + return nil, err + } - // 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) + 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() { + return nil, structs.ErrPermissionDenied + } + identity.ClientID = claims.NodeIdentityClaims.NodeID + identity.Claims = claims } - 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 } +// 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) { @@ -432,37 +513,83 @@ func (s *Authenticator) ResolveToken(secretID string) (*acl.ACL, error) { return resolveTokenFromSnapshotCache(snap, s.aclCache, secretID) } -// VerifyClaim asserts that the token is valid and that the resulting allocation -// ID belongs to a non-terminal allocation. This should usually not be called by -// RPC handlers, and exists only to support the ACL.WhoAmI endpoint. +// 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 claims.IsNode() { + if err := s.verifyNodeIdentityClaim(claims); err != nil { + return nil, err + } + return claims, nil + } + + return nil, errors.New("failed to determine claim type") +} + +func (s *Authenticator) verifyWorkloadIdentityClaim(claims *structs.IdentityClaims) error { snap, err := s.getState().Snapshot() if err != nil { - return nil, err + return err } alloc, err := snap.AllocByID(nil, claims.AllocationID) if err != nil { - return nil, err + return err } if alloc == nil || alloc.Job == nil { - return nil, fmt.Errorf("allocation does not exist") + return fmt.Errorf("allocation does not exist") } // the claims for terminal allocs are always treated as expired if alloc.ClientTerminalStatus() { - return nil, fmt.Errorf("allocation is terminal") + return fmt.Errorf("allocation is terminal") } - return claims, nil + return nil +} + +func (s *Authenticator) verifyNodeIdentityClaim(claims *structs.IdentityClaims) error { + + snap, err := s.getState().Snapshot() + if err != nil { + return err + } + node, err := snap.NodeByID(nil, claims.NodeIdentityClaims.NodeID) + if err != nil { + return err + } + if node == nil { + return errors.New("node does not exist") + } + + 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() { + return acl.ClientACL, nil + } + policies, err := s.ResolvePoliciesForClaims(claims) if err != nil { return nil, err diff --git a/nomad/auth/auth_test.go b/nomad/auth/auth_test.go index e56f50e62..b3059f6b7 100644 --- a/nomad/auth/auth_test.go +++ b/nomad/auth/auth_test.go @@ -359,6 +359,50 @@ func TestAuthenticateDefault(t *testing.T) { must.True(t, aclObj.IsManagement()) }, }, + { + name: "mTLS and ACLs with node identity", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1000, node)) + + claims := structs.GenerateNodeIdentityClaims(node, "global", 1*time.Hour) + + auth := testAuthenticator(t, store, true, true) + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + args := &structs.GenericRequest{} + args.AuthToken = token + var ctx *testContext + + must.NoError(t, auth.Authenticate(ctx, args)) + must.Eq(t, "client:"+node.ID, args.GetIdentity().String()) + + aclObj, err := auth.ResolveACL(args) + must.NoError(t, err) + must.Eq(t, acl.ClientACL, aclObj) + }, + }, + { + name: "mTLS and ACLs with invalid node identity", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + + claims := structs.GenerateNodeIdentityClaims(node, "global", 1*time.Hour) + + auth := testAuthenticator(t, store, true, true) + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + args := &structs.GenericRequest{} + args.AuthToken = token + var ctx *testContext + + must.ErrorContains(t, auth.Authenticate(ctx, args), "node does not exist") + }, + }, } for _, tc := range testCases { @@ -464,6 +508,253 @@ func TestAuthenticateServerOnly(t *testing.T) { } } +func TestAuthenticator_AuthenticateClientRegistration(t *testing.T) { + ci.Parallel(t) + + testAuthenticator := func( + t *testing.T, + store *state.StateStore, + hasACLs, + verifyTLS 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, + VerifyTLS: verifyTLS, + Region: "global", + Encrypter: newTestEncrypter(), + }) + } + + testCases := []struct { + name string + testFn func(*testing.T, *state.StateStore) + }{ + { + name: "incorrect mTLS", + testFn: func(t *testing.T, store *state.StateStore) { + ctx := newTestContext(t, "pony.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{} + + auth := testAuthenticator(t, store, false, true) + must.ErrorContains(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args), "invalid certificate") + }, + }, + { + name: "client mTLS with no auth", + testFn: func(t *testing.T, store *state.StateStore) { + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{} + + auth := testAuthenticator(t, store, false, true) + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowClientOp()) + }, + }, + { + name: "no mTLS no acl with no auth", + testFn: func(t *testing.T, store *state.StateStore) { + ctx := newTestContext(t, noTLSCtx, "192.168.1.1") + + args := structs.GenericRequest{} + + auth := testAuthenticator(t, store, false, false) + must.Nil(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.False(t, aclObj.AllowServerOp()) + must.False(t, aclObj.AllowServerOp()) + }, + }, + { + name: "no mTLS acl with no auth", + testFn: func(t *testing.T, store *state.StateStore) { + ctx := newTestContext(t, noTLSCtx, "192.168.1.1") + + args := structs.GenericRequest{} + + auth := testAuthenticator(t, store, true, false) + must.Nil(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.False(t, aclObj.AllowServerOp()) + must.False(t, aclObj.AllowServerOp()) + }, + }, + { + name: "no mTLS no acl with server leader token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + auth := testAuthenticator(t, store, false, false) + + ctx := newTestContext(t, noTLSCtx, "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: auth.getLeaderACL(), + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowServerOp() || aclObj.AllowClientOp()) + }, + }, + { + name: "mTLS acl with server leader token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + auth := testAuthenticator(t, store, true, true) + + ctx := newTestContext(t, "server.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: auth.getLeaderACL(), + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowServerOp()) + }, + }, + { + name: "mTLS no acl with server leader token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + auth := testAuthenticator(t, store, false, true) + + ctx := newTestContext(t, "server.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: auth.getLeaderACL(), + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowClientOp()) + }, + }, + { + name: "mTLS no acl with node secret token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1000, node)) + + auth := testAuthenticator(t, store, false, true) + + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: node.SecretID, + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowClientOp()) + }, + }, + { + name: "mTLS acl with node secret token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1000, node)) + + auth := testAuthenticator(t, store, true, true) + + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: node.SecretID, + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowClientOp()) + }, + }, + { + name: "mTLS acl with bad node secret token auth", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1000, node)) + + auth := testAuthenticator(t, store, true, true) + + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: node.ID, + }, + } + + must.ErrorContains(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args), "Permission denied") + }, + }, + { + name: "mTLS acl with node identity", + testFn: func(t *testing.T, store *state.StateStore) { + + node := mock.Node() + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 1000, node)) + + claims := structs.GenerateNodeIdentityClaims(node, "global", 1*time.Hour) + + auth := testAuthenticator(t, store, true, true) + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + args := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: token, + }, + } + + must.NoError(t, auth.AuthenticateNodeIdentityGenerator(ctx, &args)) + aclObj, err := auth.ResolveACL(&args) + must.NoError(t, err) + must.True(t, aclObj.AllowClientOp()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.testFn(t, testStateStore(t)) + }) + } +} + func TestAuthenticateClientOnly(t *testing.T) { ci.Parallel(t) @@ -478,7 +769,7 @@ func TestAuthenticateClientOnly(t *testing.T) { AclsEnabled: hasACLs, VerifyTLS: verifyTLS, Region: "global", - Encrypter: nil, + Encrypter: newTestEncrypter(), }) } @@ -487,7 +778,7 @@ func TestAuthenticateClientOnly(t *testing.T) { testFn func(*testing.T, *state.StateStore, *structs.Node) }{ { - name: "no mTLS or ACLs but no node secret", + name: "no mTLS or ACLs but no auth token", testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) { ctx := newTestContext(t, noTLSCtx, "192.168.1.1") args := &structs.GenericRequest{} @@ -535,7 +826,7 @@ func TestAuthenticateClientOnly(t *testing.T) { }, }, { - name: "no mTLS but with ACLs and bad secret", + name: "no mTLS but with ACLs and bad auth token", testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) { ctx := newTestContext(t, noTLSCtx, "192.168.1.1") args := &structs.GenericRequest{} @@ -567,7 +858,7 @@ func TestAuthenticateClientOnly(t *testing.T) { }, }, { - name: "with mTLS and ACLs with server cert but bad token", + name: "with mTLS and ACLs with server cert but bad auth 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{} @@ -583,7 +874,7 @@ func TestAuthenticateClientOnly(t *testing.T) { }, }, { - name: "with mTLS and ACLs with server cert and valid token", + name: "with mTLS and ACLs with server cert and valid secret ID 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{} @@ -615,13 +906,82 @@ func TestAuthenticateClientOnly(t *testing.T) { must.True(t, aclObj.AllowClientOp()) }, }, + { + name: "with mTLS and ACLs with client cert and valid node identity", + testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) { + ctx := newTestContext(t, "client.global.nomad", "192.168.1.1") + + auth := testAuthenticator(t, store, true, true) + + claims := structs.GenerateNodeIdentityClaims(node, "global", 1*time.Hour) + + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + args := &structs.GenericRequest{} + args.AuthToken = token + + 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 server cert and valid node identity", + testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) { + ctx := newTestContext(t, "server.global.nomad", "192.168.1.1") + + auth := testAuthenticator(t, store, true, true) + + claims := structs.GenerateNodeIdentityClaims(node, "global", 1*time.Hour) + + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + args := &structs.GenericRequest{} + args.AuthToken = token + + 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 server cert and invalid node identity", + testFn: func(t *testing.T, store *state.StateStore, node *structs.Node) { + ctx := newTestContext(t, "server.global.nomad", "192.168.1.1") + + auth := testAuthenticator(t, store, true, true) + + copiedNode := node.Copy() + copiedNode.ID = uuid.Generate() + + claims := structs.GenerateNodeIdentityClaims(copiedNode, "global", 1*time.Hour) + + token, err := auth.encrypter.(*testEncrypter).signClaim(claims) + must.NoError(t, err) + + args := &structs.GenericRequest{} + args.AuthToken = token + + aclObj, err := auth.AuthenticateClientOnly(ctx, args) + must.Error(t, err) + must.Nil(t, aclObj) + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { node := mock.Node() store := testStateStore(t) - store.UpsertNode(structs.MsgTypeTestSetup, 100, node) + must.NoError(t, store.UpsertNode(structs.MsgTypeTestSetup, 100, node)) tc.testFn(t, store, node) }) } @@ -1178,6 +1538,45 @@ func TestResolveClaims(t *testing.T) { } +func TestAuthenticator_verifyNodeIdentityClaim(t *testing.T) { + ci.Parallel(t) + + // Create our base test objects including a node that can be used in the + // tests. + testAuthenticator := testDefaultAuthenticator(t) + + mockNode := mock.Node() + must.NoError(t, testAuthenticator.getState().UpsertNode(structs.MsgTypeTestSetup, 100, mockNode)) + + testCases := []struct { + name string + inputClaims *structs.IdentityClaims + expectedOutput error + }{ + { + name: "node does not exist", + inputClaims: structs.GenerateNodeIdentityClaims(mock.Node(), "global", 1*time.Hour), + expectedOutput: errors.New("node does not exist"), + }, + { + name: "verified node claims", + inputClaims: structs.GenerateNodeIdentityClaims(mockNode, "global", 1*time.Hour), + expectedOutput: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualOutput := testAuthenticator.verifyNodeIdentityClaim(tc.inputClaims) + if tc.expectedOutput == nil { + must.NoError(t, actualOutput) + } else { + must.EqError(t, actualOutput, tc.expectedOutput.Error()) + } + }) + } +} + func testStateStore(t *testing.T) *state.StateStore { sconfig := &state.StateStoreConfig{ Logger: testlog.HCLogger(t), diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index aceb5fcfb..b88c20109 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -542,12 +542,15 @@ func (ai *AuthenticatedIdentity) String() string { if ai.ACLToken != nil && ai.ACLToken != AnonymousACLToken { return "token:" + ai.ACLToken.AccessorID } - if ai.Claims != nil { + if ai.Claims != nil && ai.Claims.IsWorkload() { return "alloc:" + ai.Claims.AllocationID } if ai.ClientID != "" { return "client:" + ai.ClientID } + if ai.Claims != nil && ai.Claims.IsNode() { + return "client:" + ai.Claims.NodeID + } return ai.TLSName + ":" + ai.RemoteIP.String() }