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