Add support for revoke by serial number to update the unified CRL (#18786)
This commit is contained in:
parent
0eedcd979b
commit
f3ce351e01
|
@ -118,6 +118,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
|
||||
WriteForwardedStorage: []string{
|
||||
crossRevocationPath,
|
||||
unifiedRevocationWritePathPrefix,
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -143,6 +144,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
pathRevokeWithKey(&b),
|
||||
pathListCertsRevoked(&b),
|
||||
pathListCertsRevocationQueue(&b),
|
||||
pathListUnifiedRevoked(&b),
|
||||
pathTidy(&b),
|
||||
pathTidyCancel(&b),
|
||||
pathTidyStatus(&b),
|
||||
|
|
|
@ -543,7 +543,10 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
|
|||
removalQueue := cb.removalQueue.Iterate()
|
||||
|
||||
sc.Backend.Logger().Debug(fmt.Sprintf("gathered %v revocations and %v confirmation entries", len(revQueue), len(removalQueue)))
|
||||
|
||||
crlConfig, err := cb.getConfigWithUpdate(sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, req := range revQueue {
|
||||
sc.Backend.Logger().Debug(fmt.Sprintf("handling revocation request: %v", req))
|
||||
rPath := crossRevocationPrefix + req.Cluster + "/" + req.Serial
|
||||
|
@ -558,7 +561,7 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
|
|||
continue
|
||||
}
|
||||
|
||||
resp, err := tryRevokeCertBySerial(sc, req.Serial)
|
||||
resp, err := tryRevokeCertBySerial(sc, crlConfig, req.Serial)
|
||||
sc.Backend.Logger().Debug(fmt.Sprintf("checked local revocation entry: %v / %v", resp, err))
|
||||
if err == nil && resp != nil && !resp.IsError() && resp.Data != nil && resp.Data["state"].(string) == "revoked" {
|
||||
if isNotPerfPrimary {
|
||||
|
@ -674,7 +677,7 @@ func fetchIssuerMapForRevocationChecking(sc *storageContext) (map[issuerID]*x509
|
|||
|
||||
// Revoke a certificate from a given serial number if it is present in local
|
||||
// storage.
|
||||
func tryRevokeCertBySerial(sc *storageContext, serial string) (*logical.Response, error) {
|
||||
func tryRevokeCertBySerial(sc *storageContext, config *crlConfig, serial string) (*logical.Response, error) {
|
||||
certEntry, err := fetchCertBySerial(sc, "certs/", serial)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
|
@ -694,11 +697,11 @@ func tryRevokeCertBySerial(sc *storageContext, serial string) (*logical.Response
|
|||
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
||||
}
|
||||
|
||||
return revokeCert(sc, cert)
|
||||
return revokeCert(sc, config, cert)
|
||||
}
|
||||
|
||||
// Revokes a cert, and tries to be smart about error recovery
|
||||
func revokeCert(sc *storageContext, cert *x509.Certificate) (*logical.Response, error) {
|
||||
func revokeCert(sc *storageContext, config *crlConfig, cert *x509.Certificate) (*logical.Response, error) {
|
||||
// As this backend is self-contained and this function does not hook into
|
||||
// third parties to manage users or resources, if the mount is tainted,
|
||||
// revocation doesn't matter anyways -- the CRL that would be written will
|
||||
|
@ -786,12 +789,23 @@ func revokeCert(sc *storageContext, cert *x509.Certificate) (*logical.Response,
|
|||
}
|
||||
sc.Backend.incrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)
|
||||
|
||||
// Fetch the config and see if we need to rebuild the CRL. If we have
|
||||
// auto building enabled, we will wait for the next rebuild period to
|
||||
// actually rebuild it.
|
||||
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building CRL: while updating config: %w", err)
|
||||
// If this flag is enabled after the fact, existing local entries will be published to
|
||||
// the unified storage space through a periodic function.
|
||||
if config.UnifiedCRL {
|
||||
entry := &unifiedRevocationEntry{
|
||||
SerialNumber: colonSerial,
|
||||
CertExpiration: cert.NotAfter,
|
||||
RevocationTimeUTC: revInfo.RevocationTimeUTC,
|
||||
CertificateIssuer: revInfo.CertificateIssuer,
|
||||
}
|
||||
|
||||
ignoreErr := writeUnifiedRevocationEntry(sc, entry)
|
||||
if ignoreErr != nil {
|
||||
// Just log the error if we fail to write across clusters, a separate background
|
||||
// thread will reattempt it later on as we have the local write done.
|
||||
sc.Backend.Logger().Debug("Failed to write unified revocation entry",
|
||||
"serial_number", colonSerial, "error", ignoreErr)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.AutoRebuild {
|
||||
|
|
|
@ -25,6 +25,7 @@ type crlConfig struct {
|
|||
EnableDelta bool `json:"enable_delta"`
|
||||
DeltaRebuildInterval string `json:"delta_rebuild_interval"`
|
||||
UseGlobalQueue bool `json:"cross_cluster_revocation"`
|
||||
UnifiedCRL bool `json:"unified_crl"`
|
||||
}
|
||||
|
||||
// Implicit default values for the config if it does not exist.
|
||||
|
@ -39,6 +40,7 @@ var defaultCrlConfig = crlConfig{
|
|||
EnableDelta: false,
|
||||
DeltaRebuildInterval: "15m",
|
||||
UseGlobalQueue: false,
|
||||
UnifiedCRL: false,
|
||||
}
|
||||
|
||||
func pathConfigCRL(b *backend) *framework.Path {
|
||||
|
@ -88,6 +90,13 @@ the NextUpdate field); defaults to 12 hours`,
|
|||
Description: `Whether to enable a global, cross-cluster revocation queue.
|
||||
Must be used with auto_rebuild=true.`,
|
||||
},
|
||||
"unified_crl": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `If set to true enables global replication of revocation entries,
|
||||
also enabling unified versions of OCSP and CRLs if their respective features are enabled.
|
||||
disable for CRLs and ocsp_disable for OCSP.`,
|
||||
Default: "false",
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
|
@ -125,6 +134,7 @@ func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *fram
|
|||
"enable_delta": config.EnableDelta,
|
||||
"delta_rebuild_interval": config.DeltaRebuildInterval,
|
||||
"cross_cluster_revocation": config.UseGlobalQueue,
|
||||
"unified_crl": config.UnifiedCRL,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -195,6 +205,10 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
|
|||
config.UseGlobalQueue = useGlobalQueue.(bool)
|
||||
}
|
||||
|
||||
if unifiedCrlRaw, ok := d.GetOk("unified_crl"); ok {
|
||||
config.UnifiedCRL = unifiedCrlRaw.(bool)
|
||||
}
|
||||
|
||||
expiry, _ := time.ParseDuration(config.Expiry)
|
||||
if config.AutoRebuild {
|
||||
gracePeriod, _ := time.ParseDuration(config.AutoRebuildGracePeriod)
|
||||
|
@ -260,6 +274,8 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
|
|||
"auto_rebuild_grace_period": config.AutoRebuildGracePeriod,
|
||||
"enable_delta": config.EnableDelta,
|
||||
"delta_rebuild_interval": config.DeltaRebuildInterval,
|
||||
"cross_cluster_revocation": config.UseGlobalQueue,
|
||||
"unified_crl": config.UnifiedCRL,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -152,6 +152,21 @@ func pathRotateDeltaCRL(b *backend) *framework.Path {
|
|||
}
|
||||
}
|
||||
|
||||
func pathListUnifiedRevoked(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "certs/unified-revoked/?$",
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: b.pathListUnifiedRevokedCertsHandler,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathListUnifiedRevokedHelpSyn,
|
||||
HelpDescription: pathListUnifiedRevokedHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, certPem string) (string, bool, *x509.Certificate, error) {
|
||||
// This function handles just the verification of the certificate against
|
||||
// the global issuer set, checking whether or not it is importable.
|
||||
|
@ -331,14 +346,14 @@ func (b *backend) pathRevokeWriteHandleKey(req *logical.Request, certReference *
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *backend) maybeRevokeCrossCluster(ctx context.Context, sc *storageContext, serial string) (*logical.Response, error) {
|
||||
config, err := b.crlBuilder.getConfigWithUpdate(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (b *backend) maybeRevokeCrossCluster(sc *storageContext, config *crlConfig, serial string, havePrivateKey bool) (*logical.Response, error) {
|
||||
if !config.UseGlobalQueue {
|
||||
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found.", serial)), nil
|
||||
}
|
||||
|
||||
if !config.UseGlobalQueue {
|
||||
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
|
||||
if havePrivateKey {
|
||||
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found, "+
|
||||
"and cross-cluster revocation not supported with key revocation.", serial)), nil
|
||||
}
|
||||
|
||||
// Here, we have to use the global revocation queue as the cert
|
||||
|
@ -355,7 +370,7 @@ func (b *backend) maybeRevokeCrossCluster(ctx context.Context, sc *storageContex
|
|||
return nil, fmt.Errorf("failed to create storage entry for cross-cluster revocation request: %w", err)
|
||||
}
|
||||
|
||||
if err := sc.Storage.Put(ctx, reqEntry); err != nil {
|
||||
if err := sc.Storage.Put(sc.Context, reqEntry); err != nil {
|
||||
return nil, fmt.Errorf("error persisting cross-cluster revocation request: %w\nThis may occur when the active node of the primary performance replication cluster is unavailable.", err)
|
||||
}
|
||||
|
||||
|
@ -397,8 +412,12 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
|
|||
var cert *x509.Certificate
|
||||
var serial string
|
||||
|
||||
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error revoking serial: %s: failed reading config: %w", serial, err)
|
||||
}
|
||||
|
||||
if haveCert {
|
||||
var err error
|
||||
serial, writeCert, cert, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -425,14 +444,29 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
||||
}
|
||||
} else if keyPem == "" {
|
||||
// Cross-cluster revocation can only happen without PoP currently.
|
||||
return b.maybeRevokeCrossCluster(ctx, sc, serial)
|
||||
}
|
||||
}
|
||||
|
||||
if cert == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
|
||||
if config.UnifiedCRL {
|
||||
// Saving grace if we aren't able to load the certificate locally/or were given it,
|
||||
// if we have a unified revocation entry already return its revocation times,
|
||||
// otherwise we fail with a certificate not found message.
|
||||
unifiedRev, err := getUnifiedRevocationBySerial(sc, normalizeSerial(serial))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unifiedRev != nil {
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"revocation_time": unifiedRev.RevocationTimeUTC.Unix(),
|
||||
"revocation_time_rfc3339": unifiedRev.RevocationTimeUTC.Format(time.RFC3339Nano),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return b.maybeRevokeCrossCluster(sc, config, serial, keyPem != "")
|
||||
}
|
||||
|
||||
// Before we write the certificate, we've gotta verify the request in
|
||||
|
@ -458,7 +492,7 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
|
|||
b.revokeStorageLock.Lock()
|
||||
defer b.revokeStorageLock.Unlock()
|
||||
|
||||
return revokeCert(sc, cert)
|
||||
return revokeCert(sc, config, cert)
|
||||
}
|
||||
|
||||
func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
|
@ -583,6 +617,22 @@ func (b *backend) pathListRevocationQueueHandler(ctx context.Context, request *l
|
|||
return logical.ListResponseWithInfo(responseKeys, responseInfo), nil
|
||||
}
|
||||
|
||||
func (b *backend) pathListUnifiedRevokedCertsHandler(ctx context.Context, request *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
sc := b.makeStorageContext(ctx, request.Storage)
|
||||
|
||||
revokedCerts, err := listUnifiedRevokedCerts(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Normalize serial back to a format people are expecting.
|
||||
for i, serial := range revokedCerts {
|
||||
revokedCerts[i] = denormalizeSerial(serial)
|
||||
}
|
||||
|
||||
return logical.ListResponse(revokedCerts), nil
|
||||
}
|
||||
|
||||
const pathRevokeHelpSyn = `
|
||||
Revoke a certificate by serial number or with explicit certificate.
|
||||
|
||||
|
@ -619,6 +669,14 @@ const pathListRevokedHelpDesc = `
|
|||
Returns a list of serial numbers for revoked certificates in the local cluster.
|
||||
`
|
||||
|
||||
const pathListUnifiedRevokedHelpSyn = `
|
||||
List all revoked serial numbers within this cluster's unified storage area.
|
||||
`
|
||||
|
||||
const pathListUnifiedRevokedHelpDesc = `
|
||||
Returns a list of serial numbers for revoked certificates within this cluster's unified storage.
|
||||
`
|
||||
|
||||
const pathListRevocationQueueHelpSyn = `
|
||||
List all pending, cross-cluster revocations known to the local cluster.
|
||||
`
|
||||
|
|
|
@ -50,8 +50,9 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
|
|||
defer b.revokeStorageLock.Unlock()
|
||||
|
||||
sc := b.makeStorageContext(ctx, req.Storage)
|
||||
serial := serialInt.(string)
|
||||
|
||||
certEntry, err := fetchCertBySerial(sc, "certs/", serialInt.(string))
|
||||
certEntry, err := fetchCertBySerial(sc, "certs/", serial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -59,7 +60,7 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
|
|||
// We can't write to revoked/ or update the CRL anyway because we don't have the cert,
|
||||
// and there's no reason to expect this will work on a subsequent
|
||||
// retry. Just give up and let the lease get deleted.
|
||||
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serialInt.(string))
|
||||
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serial)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -73,5 +74,10 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return revokeCert(sc, cert)
|
||||
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error revoking serial: %s: failed reading config: %w", serial, err)
|
||||
}
|
||||
|
||||
return revokeCert(sc, config, cert)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
unifiedRevocationReadPathPrefix = "unified-revocation/"
|
||||
unifiedRevocationWritePathPrefix = unifiedRevocationReadPathPrefix + "{{clusterId}}/"
|
||||
)
|
||||
|
||||
type unifiedRevocationEntry struct {
|
||||
SerialNumber string `json:"-"`
|
||||
CertExpiration time.Time `json:"certificate_expiration_utc"`
|
||||
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
||||
CertificateIssuer issuerID `json:"issuer_id"`
|
||||
}
|
||||
|
||||
func getUnifiedRevocationBySerial(sc *storageContext, serial string) (*unifiedRevocationEntry, error) {
|
||||
clusterPaths, err := lookupClusterPaths(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, path := range clusterPaths {
|
||||
serialPath := path + serial
|
||||
entryRaw, err := sc.Storage.Get(sc.Context, serialPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entryRaw != nil {
|
||||
var revEntry unifiedRevocationEntry
|
||||
if err := entryRaw.DecodeJSON(&revEntry); err != nil {
|
||||
return nil, fmt.Errorf("failed json decoding of unified entry at path %s: %w", serialPath, err)
|
||||
}
|
||||
revEntry.SerialNumber = serial
|
||||
return &revEntry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func writeUnifiedRevocationEntry(sc *storageContext, ure *unifiedRevocationEntry) error {
|
||||
json, err := logical.StorageEntryJSON(unifiedRevocationWritePathPrefix+normalizeSerial(ure.SerialNumber), ure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sc.Storage.Put(sc.Context, json)
|
||||
}
|
||||
|
||||
func listUnifiedRevokedCerts(sc *storageContext) ([]string, error) {
|
||||
allSerials := []string{}
|
||||
|
||||
clusterPaths, err := lookupClusterPaths(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, path := range clusterPaths {
|
||||
clusterSerials, err := sc.Storage.List(sc.Context, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed listing revoked certs for path %s: %w", path, err)
|
||||
}
|
||||
|
||||
allSerials = append(allSerials, clusterSerials...)
|
||||
}
|
||||
return allSerials, nil
|
||||
}
|
||||
|
||||
func lookupClusterPaths(sc *storageContext) ([]string, error) {
|
||||
fullPaths := []string{}
|
||||
|
||||
clusterPaths, err := sc.Storage.List(sc.Context, unifiedRevocationReadPathPrefix)
|
||||
if err != nil {
|
||||
return fullPaths, err
|
||||
}
|
||||
|
||||
for _, clusterId := range clusterPaths {
|
||||
fullPaths = append(fullPaths, unifiedRevocationReadPathPrefix+clusterId)
|
||||
}
|
||||
|
||||
return fullPaths, nil
|
||||
}
|
Loading…
Reference in New Issue