mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 08:55:43 +03:00
When a root key is rotated, the servers immediately start signing Workload Identities with the new active key. But workloads may be using those WI tokens to sign into external services, which may not have had time to fetch the new public key and which might try to fetch new keys as needed. Add support for prepublishing keys. Prepublished keys will be visible in the JWKS endpoint but will not be used for signing or encryption until their `PublishTime`. Update the periodic key rotation to prepublish keys at half the `root_key_rotation_threshold` window, and promote prepublished keys to active after the `PublishTime`. This changeset also fixes two bugs in periodic root key rotation and garbage collection, both of which can't be safely fixed without implementing prepublishing: * Periodic root key rotation would never happen because the default `root_key_rotation_threshold` of 720h exceeds the 72h maximum window of the FSM time table. We now compare the `CreateTime` against the wall clock time instead of the time table. (We expect to remove the time table in future work, ref https://github.com/hashicorp/nomad/issues/16359) * Root key garbage collection could GC keys that were used to sign identities. We now wait until `root_key_rotation_threshold` + `root_key_gc_threshold` before GC'ing a key. * When rekeying a root key, the core job did not mark the key as inactive after the rekey was complete. Ref: https://hashicorp.atlassian.net/browse/NET-10398 Ref: https://hashicorp.atlassian.net/browse/NET-10280 Fixes: https://github.com/hashicorp/nomad/issues/19669 Fixes: https://github.com/hashicorp/nomad/issues/23528 Fixes: https://github.com/hashicorp/nomad/issues/19368
200 lines
5.7 KiB
Go
200 lines
5.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v3"
|
|
"github.com/hashicorp/nomad/helper"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
// jwksMinMaxAge is the minimum amount of time the JWKS endpoint will instruct
|
|
// consumers to cache a response for.
|
|
const jwksMinMaxAge = 15 * time.Minute
|
|
|
|
// JWKSRequest is used to handle JWKS requests. JWKS stands for JSON Web Key
|
|
// Sets and returns the public keys used for signing workload identities. Third
|
|
// parties may use this endpoint to validate workload identities. Consumers
|
|
// should cache this endpoint, preferably until an unknown kid is encountered.
|
|
func (s *HTTPServer) JWKSRequest(resp http.ResponseWriter, req *http.Request) (any, error) {
|
|
if req.Method != http.MethodGet {
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
}
|
|
|
|
args := structs.GenericRequest{}
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
return nil, nil
|
|
}
|
|
|
|
var rpcReply structs.KeyringListPublicResponse
|
|
if err := s.agent.RPC("Keyring.ListPublic", &args, &rpcReply); err != nil {
|
|
return nil, err
|
|
}
|
|
setMeta(resp, &rpcReply.QueryMeta)
|
|
|
|
// Key set will change after max(CreateTime) + RotationThreshold.
|
|
var newestKey int64
|
|
jwks := make([]jose.JSONWebKey, 0, len(rpcReply.PublicKeys))
|
|
for _, pubKey := range rpcReply.PublicKeys {
|
|
if pubKey.CreateTime > newestKey {
|
|
newestKey = pubKey.CreateTime
|
|
}
|
|
|
|
jwk := jose.JSONWebKey{
|
|
KeyID: pubKey.KeyID,
|
|
Algorithm: pubKey.Algorithm,
|
|
Use: pubKey.Use,
|
|
}
|
|
|
|
// Convert public key bytes to an ed25519 public key
|
|
if k, err := pubKey.GetPublicKey(); err == nil {
|
|
jwk.Key = k
|
|
} else {
|
|
s.logger.Warn("error getting public key. server is likely newer than client", "error", err)
|
|
continue
|
|
}
|
|
|
|
jwks = append(jwks, jwk)
|
|
}
|
|
|
|
// Have nonzero create times and threshold so set a reasonable cache time.
|
|
if newestKey > 0 && rpcReply.RotationThreshold > 0 {
|
|
exp := time.Unix(0, newestKey).Add(rpcReply.RotationThreshold)
|
|
maxAge := helper.ExpiryToRenewTime(exp, time.Now, jwksMinMaxAge)
|
|
resp.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", int(maxAge.Seconds())))
|
|
}
|
|
|
|
out := &jose.JSONWebKeySet{
|
|
Keys: jwks,
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// OIDCDiscoveryRequest implements the OIDC Discovery protocol for using
|
|
// workload identity JWTs with external services.
|
|
//
|
|
// See https://openid.net/specs/openid-connect-discovery-1_0.html for details.
|
|
func (s *HTTPServer) OIDCDiscoveryRequest(resp http.ResponseWriter, req *http.Request) (any, error) {
|
|
if req.Method != http.MethodGet {
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
}
|
|
|
|
args := structs.GenericRequest{}
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
return nil, nil
|
|
}
|
|
var rpcReply structs.KeyringGetConfigResponse
|
|
if err := s.agent.RPC("Keyring.GetConfig", &args, &rpcReply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rpcReply.OIDCDiscovery == nil {
|
|
return nil, CodedError(http.StatusNotFound, "OIDC Discovery endpoint disabled")
|
|
}
|
|
|
|
return rpcReply.OIDCDiscovery, nil
|
|
}
|
|
|
|
// KeyringRequest is used route operator/raft API requests to the implementing
|
|
// functions.
|
|
func (s *HTTPServer) KeyringRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
path := strings.TrimPrefix(req.URL.Path, "/v1/operator/keyring/")
|
|
switch {
|
|
case strings.HasPrefix(path, "keys"):
|
|
if req.Method != http.MethodGet {
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
}
|
|
return s.keyringListRequest(resp, req)
|
|
case strings.HasPrefix(path, "key"):
|
|
keyID := strings.TrimPrefix(req.URL.Path, "/v1/operator/keyring/key/")
|
|
switch req.Method {
|
|
case http.MethodDelete:
|
|
return s.keyringDeleteRequest(resp, req, keyID)
|
|
default:
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
}
|
|
case strings.HasPrefix(path, "rotate"):
|
|
switch req.Method {
|
|
case http.MethodPost, http.MethodPut:
|
|
return s.keyringRotateRequest(resp, req)
|
|
default:
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
}
|
|
default:
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
}
|
|
}
|
|
|
|
func (s *HTTPServer) keyringListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
args := structs.KeyringListRootKeyMetaRequest{}
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
return nil, nil
|
|
}
|
|
|
|
var out structs.KeyringListRootKeyMetaResponse
|
|
if err := s.agent.RPC("Keyring.List", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
if out.Keys == nil {
|
|
out.Keys = make([]*structs.RootKeyMeta, 0)
|
|
}
|
|
return out.Keys, nil
|
|
}
|
|
|
|
func (s *HTTPServer) keyringRotateRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
args := structs.KeyringRotateRootKeyRequest{}
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
query := req.URL.Query()
|
|
switch query.Get("algo") {
|
|
case string(structs.EncryptionAlgorithmAES256GCM):
|
|
args.Algorithm = structs.EncryptionAlgorithmAES256GCM
|
|
}
|
|
|
|
if _, ok := query["full"]; ok {
|
|
args.Full = true
|
|
}
|
|
|
|
ptRaw := query.Get("publish_time")
|
|
if ptRaw != "" {
|
|
publishTime, err := strconv.ParseInt(ptRaw, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid publish_time: %w", err)
|
|
}
|
|
args.PublishTime = publishTime
|
|
}
|
|
|
|
var out structs.KeyringRotateRootKeyResponse
|
|
if err := s.agent.RPC("Keyring.Rotate", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
setIndex(resp, out.Index)
|
|
return out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) keyringDeleteRequest(resp http.ResponseWriter, req *http.Request, keyID string) (interface{}, error) {
|
|
|
|
args := structs.KeyringDeleteRootKeyRequest{KeyID: keyID}
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
var out structs.KeyringDeleteRootKeyResponse
|
|
if err := s.agent.RPC("Keyring.Delete", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
setIndex(resp, out.Index)
|
|
return out, nil
|
|
}
|