Files
nomad/client/widmgr/signer.go
James Rasell 8096ea4129 client: Handle identities from servers and use for RPC auth. (#26218)
Nomad servers, if upgraded, can return node identities as part of
the register and update/heartbeat response objects. The Nomad
client will now handle this and store it as appropriate within its
memory and statedb.

The client will now use any stored identity for RPC authentication
with a fallback to the secretID. This supports upgrades paths where
the Nomad clients are updated before the Nomad servers.
2025-07-14 14:24:43 +01:00

127 lines
3.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package widmgr
import (
"fmt"
"sync/atomic"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/nomad/structs"
)
type RPCer interface {
RPC(method string, args any, reply any) error
}
// IdentitySigner is the interface needed to retrieve signed identities for
// workload identities. At runtime it is implemented by *widmgr.Signer.
type IdentitySigner interface {
SignIdentities(minIndex uint64, req []*structs.WorkloadIdentityRequest) ([]*structs.SignedWorkloadIdentity, error)
}
// SignerConfig wraps the configuration parameters the workload identity manager
// needs.
type SignerConfig struct {
// NodeSecret is the node's secret token
NodeSecret string
// Region of the node
Region string
RPC RPCer
}
// Signer fetches and validates workload identities.
type Signer struct {
nodeSecret string
nodeIdentityToken atomic.Value
region string
rpc RPCer
}
// NewSigner workload identity manager.
func NewSigner(c SignerConfig) *Signer {
return &Signer{
nodeSecret: c.NodeSecret,
region: c.Region,
rpc: c.RPC,
}
}
// SetNodeIdentityToken fulfills the NodeIdentityHandler interface, allowing
// the client to update the node identity token used for RPC calls when it is
// renewed.
func (s *Signer) SetNodeIdentityToken(token string) { s.nodeIdentityToken.Store(token) }
// SignIdentities wraps the Alloc.SignIdentities RPC and retrieves signed
// workload identities. The minIndex should be set to the lowest allocation
// CreateIndex to ensure that the server handling the request isn't so stale
// that it doesn't know the allocation exist (and therefore rejects the signing
// requests).
//
// Since a single rejection causes an error to be returned, SignIdentities
// should currently only be used when requesting signed identities for a single
// allocation.
func (s *Signer) SignIdentities(minIndex uint64, req []*structs.WorkloadIdentityRequest) ([]*structs.SignedWorkloadIdentity, error) {
if len(req) == 0 {
return nil, fmt.Errorf("no identities to sign")
}
// Default to using the node secret, but if the node identity token is set,
// this will be used instead. This handles the case where the node is
// upgraded before the Nomad servers and should be removed in Nomad 1.13.
authToken := s.nodeSecret
if id := s.nodeIdentityToken.Load(); id != nil {
authToken = id.(string)
}
args := structs.AllocIdentitiesRequest{
Identities: req,
QueryOptions: structs.QueryOptions{
Region: s.region,
// Unlike other RPCs, this one doesn't care about "subsequent
// modifications" after an index. We only want to ensure the state
// isn't too stale to know about this alloc, so we instruct the
// Server to block at least until the Allocation is created.
MinQueryIndex: minIndex - 1,
AllowStale: true,
AuthToken: authToken,
},
}
reply := structs.AllocIdentitiesResponse{}
if err := s.rpc.RPC("Alloc.SignIdentities", &args, &reply); err != nil {
return nil, err
}
if n := len(reply.Rejections); n == 1 {
return nil, fmt.Errorf(
"%d/%d signing request was rejected: %v",
n, len(req), reply.Rejections[0].Reason,
)
} else if n > 1 {
var mErr *multierror.Error
for _, r := range reply.Rejections {
mErr = multierror.Append(
fmt.Errorf(
"%d/%d signing request was rejected: %v",
n, len(req), r.Reason,
))
}
return nil, mErr
}
if len(reply.SignedIdentities) == 0 {
return nil, fmt.Errorf("empty signed identity response")
}
if exp, act := len(reply.SignedIdentities), len(req); exp != act {
return nil, fmt.Errorf("expected %d signed identities but received %d", exp, act)
}
return reply.SignedIdentities, nil
}