Files
nomad/client/widmgr/signer.go
Piotr Kazmierczak 86d2cdcf80 client: split identity_hook across allocrunner and taskrunner (#18431)
This commit splits identity_hook between the allocrunner and taskrunner. The
allocrunner-level part of the hook signs each task identity, and the
taskrunner-level part picks it up and stores secrets for each task.

The code revamps the WIDMgr, which is now split into 2 interfaces:
IdentityManager which manages renewals of signatures and handles sending
updates to subscribers via Watch method, and IdentitySigner which only does the
signing.

This work is necessary for having a unified Consul login workflow that comes
with the new Consul integration. A new, allocrunner-level consul_hook will now
be the only hook doing Consul authentication.
2023-09-21 17:31:27 +02:00

111 lines
3.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package widmgr
import (
"fmt"
"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
region string
rpc RPCer
}
// NewSigner workload identity manager.
func NewSigner(c SignerConfig) *Signer {
return &Signer{
nodeSecret: c.NodeSecret,
region: c.Region,
rpc: c.RPC,
}
}
// 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")
}
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: s.nodeSecret,
},
}
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
}