open-vault/builtin/logical/pki/path_tidy.go

2098 lines
73 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical"
)
var tidyCancelledError = errors.New("tidy operation cancelled")
type tidyStatusState int
const (
tidyStatusInactive tidyStatusState = iota
tidyStatusStarted = iota
tidyStatusFinished = iota
tidyStatusError = iota
tidyStatusCancelling = iota
tidyStatusCancelled = iota
)
type tidyStatus struct {
// Parameters used to initiate the operation
safetyBuffer int
issuerSafetyBuffer int
revQueueSafetyBuffer int
acmeAccountSafetyBuffer int
tidyCertStore bool
tidyRevokedCerts bool
tidyRevokedAssocs bool
tidyExpiredIssuers bool
tidyBackupBundle bool
tidyRevocationQueue bool
tidyCrossRevokedCerts bool
tidyAcme bool
pauseDuration string
// Status
state tidyStatusState
err error
timeStarted time.Time
timeFinished time.Time
message string
// These counts use a custom incrementer that grab and release
// a lock prior to reading.
certStoreDeletedCount uint
revokedCertDeletedCount uint
missingIssuerCertCount uint
revQueueDeletedCount uint
crossRevokedDeletedCount uint
acmeAccountsCount uint
acmeAccountsRevokedCount uint
acmeAccountsDeletedCount uint
acmeOrdersDeletedCount uint
}
type tidyConfig struct {
// AutoTidy config
Enabled bool `json:"enabled"`
Interval time.Duration `json:"interval_duration"`
// Tidy Operations
CertStore bool `json:"tidy_cert_store"`
RevokedCerts bool `json:"tidy_revoked_certs"`
IssuerAssocs bool `json:"tidy_revoked_cert_issuer_associations"`
ExpiredIssuers bool `json:"tidy_expired_issuers"`
BackupBundle bool `json:"tidy_move_legacy_ca_bundle"`
RevocationQueue bool `json:"tidy_revocation_queue"`
CrossRevokedCerts bool `json:"tidy_cross_cluster_revoked_certs"`
TidyAcme bool `json:"tidy_acme"`
// Safety Buffers
SafetyBuffer time.Duration `json:"safety_buffer"`
IssuerSafetyBuffer time.Duration `json:"issuer_safety_buffer"`
QueueSafetyBuffer time.Duration `json:"revocation_queue_safety_buffer"`
AcmeAccountSafetyBuffer time.Duration `json:"acme_account_safety_buffer"`
PauseDuration time.Duration `json:"pause_duration"`
// Metrics.
MaintainCount bool `json:"maintain_stored_certificate_counts"`
PublishMetrics bool `json:"publish_stored_certificate_count_metrics"`
}
func (tc *tidyConfig) IsAnyTidyEnabled() bool {
return tc.CertStore || tc.RevokedCerts || tc.IssuerAssocs || tc.ExpiredIssuers || tc.BackupBundle || tc.TidyAcme || tc.CrossRevokedCerts || tc.RevocationQueue
}
func (tc *tidyConfig) AnyTidyConfig() string {
return "tidy_cert_store / tidy_revoked_certs / tidy_revoked_cert_issuer_associations / tidy_expired_issuers / tidy_move_legacy_ca_bundle / tidy_revocation_queue / tidy_cross_cluster_revoked_certs / tidy_acme"
}
var defaultTidyConfig = tidyConfig{
Enabled: false,
Interval: 12 * time.Hour,
CertStore: false,
RevokedCerts: false,
IssuerAssocs: false,
ExpiredIssuers: false,
BackupBundle: false,
TidyAcme: false,
SafetyBuffer: 72 * time.Hour,
IssuerSafetyBuffer: 365 * 24 * time.Hour,
AcmeAccountSafetyBuffer: 30 * 24 * time.Hour,
PauseDuration: 0 * time.Second,
MaintainCount: false,
PublishMetrics: false,
RevocationQueue: false,
QueueSafetyBuffer: 48 * time.Hour,
CrossRevokedCerts: false,
}
func pathTidy(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "tidy",
},
Fields: addTidyFields(map[string]*framework.FieldSchema{}),
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathTidyWrite,
Responses: map[int][]framework.Response{
http.StatusAccepted: {{
Description: "Accepted",
Fields: map[string]*framework.FieldSchema{},
}},
},
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathTidyHelpSyn,
HelpDescription: pathTidyHelpDesc,
}
}
func pathTidyCancel(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy-cancel$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "tidy",
OperationSuffix: "cancel",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathTidyCancelWrite,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer time duration`,
Required: false,
},
"issuer_safety_buffer": {
Type: framework.TypeInt,
Description: `Issuer safety buffer`,
Required: false,
},
"revocation_queue_safety_buffer": {
Type: framework.TypeInt,
Description: `Revocation queue safety buffer`,
Required: true,
},
"tidy_cert_store": {
Type: framework.TypeBool,
Description: `Tidy certificate store`,
Required: false,
},
"tidy_revoked_certs": {
Type: framework.TypeBool,
Description: `Tidy revoked certificates`,
Required: false,
},
"tidy_revoked_cert_issuer_associations": {
Type: framework.TypeBool,
Description: `Tidy revoked certificate issuer associations`,
Required: false,
},
"tidy_acme": {
Type: framework.TypeBool,
Description: `Tidy Unused Acme Accounts, and Orders`,
Required: false,
},
"acme_account_safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer after creation after which accounts lacking orders are revoked`,
Required: false,
},
"tidy_expired_issuers": {
Type: framework.TypeBool,
Description: `Tidy expired issuers`,
Required: false,
},
"pause_duration": {
Type: framework.TypeString,
Description: `Duration to pause between tidying certificates`,
Required: false,
},
"state": {
Type: framework.TypeString,
Description: `One of Inactive, Running, Finished, or Error`,
Required: false,
},
"error": {
Type: framework.TypeString,
Description: `The error message`,
Required: false,
},
"time_started": {
Type: framework.TypeString,
Description: `Time the operation started`,
Required: false,
},
"time_finished": {
Type: framework.TypeString,
Description: `Time the operation finished`,
Required: false,
},
"last_auto_tidy_finished": {
Type: framework.TypeString,
Description: `Time the last auto-tidy operation finished`,
Required: true,
},
"message": {
Type: framework.TypeString,
Description: `Message of the operation`,
Required: false,
},
"cert_store_deleted_count": {
Type: framework.TypeInt,
Description: `The number of certificate storage entries deleted`,
Required: false,
},
"revoked_cert_deleted_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: false,
},
"current_cert_store_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: false,
},
"current_revoked_cert_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: false,
},
"missing_issuer_cert_count": {
Type: framework.TypeInt,
Required: false,
},
"tidy_move_legacy_ca_bundle": {
Type: framework.TypeBool,
Required: false,
},
"tidy_cross_cluster_revoked_certs": {
Type: framework.TypeBool,
Description: `Tidy the cross-cluster revoked certificate store`,
Required: false,
},
"tidy_revocation_queue": {
Type: framework.TypeBool,
Required: false,
},
"revocation_queue_deleted_count": {
Type: framework.TypeInt,
Required: false,
},
"cross_revoked_cert_deleted_count": {
Type: framework.TypeInt,
Required: false,
},
"internal_backend_uuid": {
Type: framework.TypeString,
Required: false,
},
"total_acme_account_count": {
Type: framework.TypeInt,
Description: `Total number of acme accounts iterated over`,
Required: false,
},
"acme_account_deleted_count": {
Type: framework.TypeInt,
Description: `The number of revoked acme accounts removed`,
Required: false,
},
"acme_account_revoked_count": {
Type: framework.TypeInt,
Description: `The number of unused acme accounts revoked`,
Required: false,
},
"acme_orders_deleted_count": {
Type: framework.TypeInt,
Description: `The number of expired, unused acme orders removed`,
Required: false,
},
},
}},
},
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathTidyCancelHelpSyn,
HelpDescription: pathTidyCancelHelpDesc,
}
}
func pathTidyStatus(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy-status$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "tidy",
OperationSuffix: "status",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathTidyStatusRead,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer time duration`,
Required: true,
},
"issuer_safety_buffer": {
Type: framework.TypeInt,
Description: `Issuer safety buffer`,
Required: true,
},
"revocation_queue_safety_buffer": {
Type: framework.TypeInt,
Description: `Revocation queue safety buffer`,
Required: true,
},
"acme_account_safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer after creation after which accounts lacking orders are revoked`,
Required: false,
},
"tidy_cert_store": {
Type: framework.TypeBool,
Description: `Tidy certificate store`,
Required: true,
},
"tidy_revoked_certs": {
Type: framework.TypeBool,
Description: `Tidy revoked certificates`,
Required: true,
},
"tidy_revoked_cert_issuer_associations": {
Type: framework.TypeBool,
Description: `Tidy revoked certificate issuer associations`,
Required: true,
},
"tidy_expired_issuers": {
Type: framework.TypeBool,
Description: `Tidy expired issuers`,
Required: true,
},
"tidy_cross_cluster_revoked_certs": {
Type: framework.TypeBool,
Description: `Tidy the cross-cluster revoked certificate store`,
Required: false,
},
"tidy_acme": {
Type: framework.TypeBool,
Description: `Tidy Unused Acme Accounts, and Orders`,
Required: true,
},
"pause_duration": {
Type: framework.TypeString,
Description: `Duration to pause between tidying certificates`,
Required: true,
},
"state": {
Type: framework.TypeString,
Description: `One of Inactive, Running, Finished, or Error`,
Required: true,
},
"error": {
Type: framework.TypeString,
Description: `The error message`,
Required: true,
},
"time_started": {
Type: framework.TypeString,
Description: `Time the operation started`,
Required: true,
},
"time_finished": {
Type: framework.TypeString,
Description: `Time the operation finished`,
Required: false,
},
"last_auto_tidy_finished": {
Type: framework.TypeString,
Description: `Time the last auto-tidy operation finished`,
Required: true,
},
"message": {
Type: framework.TypeString,
Description: `Message of the operation`,
Required: true,
},
"cert_store_deleted_count": {
Type: framework.TypeInt,
Description: `The number of certificate storage entries deleted`,
Required: true,
},
"revoked_cert_deleted_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: true,
},
"current_cert_store_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: true,
},
"cross_revoked_cert_deleted_count": {
Type: framework.TypeInt,
Description: ``,
Required: true,
},
"current_revoked_cert_count": {
Type: framework.TypeInt,
Description: `The number of revoked certificate entries deleted`,
Required: true,
},
"revocation_queue_deleted_count": {
Type: framework.TypeInt,
Required: true,
},
"tidy_move_legacy_ca_bundle": {
Type: framework.TypeBool,
Required: true,
},
"tidy_revocation_queue": {
Type: framework.TypeBool,
Required: true,
},
"missing_issuer_cert_count": {
Type: framework.TypeInt,
Required: true,
},
"internal_backend_uuid": {
Type: framework.TypeString,
Required: true,
},
"total_acme_account_count": {
Type: framework.TypeInt,
Description: `Total number of acme accounts iterated over`,
Required: false,
},
"acme_account_deleted_count": {
Type: framework.TypeInt,
Description: `The number of revoked acme accounts removed`,
Required: false,
},
"acme_account_revoked_count": {
Type: framework.TypeInt,
Description: `The number of unused acme accounts revoked`,
Required: false,
},
"acme_orders_deleted_count": {
Type: framework.TypeInt,
Description: `The number of expired, unused acme orders removed`,
Required: false,
},
},
}},
},
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathTidyStatusHelpSyn,
HelpDescription: pathTidyStatusHelpDesc,
}
}
func pathConfigAutoTidy(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/auto-tidy",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
},
Fields: addTidyFields(map[string]*framework.FieldSchema{
"enabled": {
Type: framework.TypeBool,
Description: `Set to true to enable automatic tidy operations.`,
},
"interval_duration": {
Type: framework.TypeDurationSecond,
Description: `Interval at which to run an auto-tidy operation. This is the time between tidy invocations (after one finishes to the start of the next). Running a manual tidy will reset this duration.`,
Default: int(defaultTidyConfig.Interval / time.Second), // TypeDurationSecond currently requires the default to be an int.
},
"maintain_stored_certificate_counts": {
Type: framework.TypeBool,
Description: `This configures whether stored certificates
are counted upon initialization of the backend, and whether during
normal operation, a running count of certificates stored is maintained.`,
Default: false,
},
"publish_stored_certificate_count_metrics": {
Type: framework.TypeBool,
Description: `This configures whether the stored certificate
count is published to the metrics consumer. It does not affect if the
stored certificate count is maintained, and if maintained, it will be
available on the tidy-status endpoint.`,
Default: false,
},
}),
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathConfigAutoTidyRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationSuffix: "auto-tidy-configuration",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"enabled": {
Type: framework.TypeBool,
Description: `Specifies whether automatic tidy is enabled or not`,
Required: true,
},
"interval_duration": {
Type: framework.TypeInt,
Description: `Specifies the duration between automatic tidy operation`,
Required: true,
},
"tidy_cert_store": {
Type: framework.TypeBool,
Description: `Specifies whether to tidy up the certificate store`,
Required: true,
},
"tidy_revoked_certs": {
Type: framework.TypeBool,
Description: `Specifies whether to remove all invalid and expired certificates from storage`,
Required: true,
},
"tidy_revoked_cert_issuer_associations": {
Type: framework.TypeBool,
Description: `Specifies whether to associate revoked certificates with their corresponding issuers`,
Required: true,
},
"tidy_expired_issuers": {
Type: framework.TypeBool,
Description: `Specifies whether tidy expired issuers`,
Required: true,
},
"tidy_acme": {
Type: framework.TypeBool,
Description: `Tidy Unused Acme Accounts, and Orders`,
Required: true,
},
"safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer time duration`,
Required: true,
},
"issuer_safety_buffer": {
Type: framework.TypeInt,
Description: `Issuer safety buffer`,
Required: true,
},
"acme_account_safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer after creation after which accounts lacking orders are revoked`,
Required: false,
},
"pause_duration": {
Type: framework.TypeString,
Description: `Duration to pause between tidying certificates`,
Required: true,
},
"tidy_move_legacy_ca_bundle": {
Type: framework.TypeBool,
Required: true,
},
"tidy_cross_cluster_revoked_certs": {
Type: framework.TypeBool,
Required: true,
},
"tidy_revocation_queue": {
Type: framework.TypeBool,
Required: true,
},
"revocation_queue_safety_buffer": {
Type: framework.TypeInt,
Required: true,
},
"publish_stored_certificate_count_metrics": {
Type: framework.TypeBool,
Required: true,
},
"maintain_stored_certificate_counts": {
Type: framework.TypeBool,
Required: true,
},
},
}},
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathConfigAutoTidyWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
OperationSuffix: "auto-tidy",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"enabled": {
Type: framework.TypeBool,
Description: `Specifies whether automatic tidy is enabled or not`,
Required: true,
},
"interval_duration": {
Type: framework.TypeInt,
Description: `Specifies the duration between automatic tidy operation`,
Required: true,
},
"tidy_cert_store": {
Type: framework.TypeBool,
Description: `Specifies whether to tidy up the certificate store`,
Required: true,
},
"tidy_revoked_certs": {
Type: framework.TypeBool,
Description: `Specifies whether to remove all invalid and expired certificates from storage`,
Required: true,
},
"tidy_revoked_cert_issuer_associations": {
Type: framework.TypeBool,
Description: `Specifies whether to associate revoked certificates with their corresponding issuers`,
Required: true,
},
"tidy_expired_issuers": {
Type: framework.TypeBool,
Description: `Specifies whether tidy expired issuers`,
Required: true,
},
"tidy_acme": {
Type: framework.TypeBool,
Description: `Tidy Unused Acme Accounts, and Orders`,
Required: true,
},
"safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer time duration`,
Required: true,
},
"issuer_safety_buffer": {
Type: framework.TypeInt,
Description: `Issuer safety buffer`,
Required: true,
},
"acme_account_safety_buffer": {
Type: framework.TypeInt,
Description: `Safety buffer after creation after which accounts lacking orders are revoked`,
Required: true,
},
"pause_duration": {
Type: framework.TypeString,
Description: `Duration to pause between tidying certificates`,
Required: true,
},
"tidy_cross_cluster_revoked_certs": {
Type: framework.TypeBool,
Description: `Tidy the cross-cluster revoked certificate store`,
Required: true,
},
"tidy_revocation_queue": {
Type: framework.TypeBool,
Required: true,
},
"tidy_move_legacy_ca_bundle": {
Type: framework.TypeBool,
Required: true,
},
"revocation_queue_safety_buffer": {
Type: framework.TypeInt,
Required: true,
},
"publish_stored_certificate_count_metrics": {
Type: framework.TypeBool,
Required: true,
},
"maintain_stored_certificate_counts": {
Type: framework.TypeBool,
Required: true,
},
},
}},
},
// Read more about why these flags are set in backend.go.
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: pathConfigAutoTidySyn,
HelpDescription: pathConfigAutoTidyDesc,
}
}
func (b *backend) pathTidyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
safetyBuffer := d.Get("safety_buffer").(int)
tidyCertStore := d.Get("tidy_cert_store").(bool)
tidyRevokedCerts := d.Get("tidy_revoked_certs").(bool) || d.Get("tidy_revocation_list").(bool)
tidyRevokedAssocs := d.Get("tidy_revoked_cert_issuer_associations").(bool)
tidyExpiredIssuers := d.Get("tidy_expired_issuers").(bool)
tidyBackupBundle := d.Get("tidy_move_legacy_ca_bundle").(bool)
issuerSafetyBuffer := d.Get("issuer_safety_buffer").(int)
pauseDurationStr := d.Get("pause_duration").(string)
pauseDuration := 0 * time.Second
tidyRevocationQueue := d.Get("tidy_revocation_queue").(bool)
queueSafetyBuffer := d.Get("revocation_queue_safety_buffer").(int)
tidyCrossRevokedCerts := d.Get("tidy_cross_cluster_revoked_certs").(bool)
tidyAcme := d.Get("tidy_acme").(bool)
acmeAccountSafetyBuffer := d.Get("acme_account_safety_buffer").(int)
if safetyBuffer < 1 {
return logical.ErrorResponse("safety_buffer must be greater than zero"), nil
}
if issuerSafetyBuffer < 1 {
return logical.ErrorResponse("issuer_safety_buffer must be greater than zero"), nil
}
if queueSafetyBuffer < 1 {
return logical.ErrorResponse("revocation_queue_safety_buffer must be greater than zero"), nil
}
if acmeAccountSafetyBuffer < 1 {
return logical.ErrorResponse("acme_account_safety_buffer must be greater than zero"), nil
}
if pauseDurationStr != "" {
var err error
pauseDuration, err = parseutil.ParseDurationSecond(pauseDurationStr)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error parsing pause_duration: %v", err)), nil
}
if pauseDuration < (0 * time.Second) {
return logical.ErrorResponse("received invalid, negative pause_duration"), nil
}
}
bufferDuration := time.Duration(safetyBuffer) * time.Second
issuerBufferDuration := time.Duration(issuerSafetyBuffer) * time.Second
queueSafetyBufferDuration := time.Duration(queueSafetyBuffer) * time.Second
acmeAccountSafetyBufferDuration := time.Duration(acmeAccountSafetyBuffer) * time.Second
// Manual run with constructed configuration.
config := &tidyConfig{
Enabled: true,
Interval: 0 * time.Second,
CertStore: tidyCertStore,
RevokedCerts: tidyRevokedCerts,
IssuerAssocs: tidyRevokedAssocs,
ExpiredIssuers: tidyExpiredIssuers,
BackupBundle: tidyBackupBundle,
SafetyBuffer: bufferDuration,
IssuerSafetyBuffer: issuerBufferDuration,
PauseDuration: pauseDuration,
RevocationQueue: tidyRevocationQueue,
QueueSafetyBuffer: queueSafetyBufferDuration,
CrossRevokedCerts: tidyCrossRevokedCerts,
TidyAcme: tidyAcme,
AcmeAccountSafetyBuffer: acmeAccountSafetyBufferDuration,
}
if !atomic.CompareAndSwapUint32(b.tidyCASGuard, 0, 1) {
resp := &logical.Response{}
resp.AddWarning("Tidy operation already in progress.")
return resp, nil
}
// Tests using framework will screw up the storage so make a locally
// scoped req to hold a reference
req = &logical.Request{
Storage: req.Storage,
}
// Mark the last tidy operation as relatively recent, to ensure we don't
// try to trigger the periodic function.
b.tidyStatusLock.Lock()
b.lastTidy = time.Now()
b.tidyStatusLock.Unlock()
// Kick off the actual tidy.
b.startTidyOperation(req, config)
resp := &logical.Response{}
if !config.IsAnyTidyEnabled() {
resp.AddWarning("Manual tidy requested but no tidy operations were set. Enable at least one tidy operation to be run (" + config.AnyTidyConfig() + ").")
} else {
resp.AddWarning("Tidy operation successfully started. Any information from the operation will be printed to Vault's server logs.")
}
if tidyRevocationQueue || tidyCrossRevokedCerts {
isNotPerfPrimary := b.System().ReplicationState().HasState(consts.ReplicationDRSecondary|consts.ReplicationPerformanceStandby) ||
(!b.System().LocalMount() && b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary))
if isNotPerfPrimary {
resp.AddWarning("tidy_revocation_queue=true and tidy_cross_cluster_revoked_certs=true can only be set on the active node of the primary cluster unless a local mount is used; this option has been ignored.")
}
}
return logical.RespondWithStatusCode(resp, req, http.StatusAccepted)
}
func (b *backend) startTidyOperation(req *logical.Request, config *tidyConfig) {
go func() {
atomic.StoreUint32(b.tidyCancelCAS, 0)
defer atomic.StoreUint32(b.tidyCASGuard, 0)
b.tidyStatusStart(config)
// Don't cancel when the original client request goes away.
ctx := context.Background()
logger := b.Logger().Named("tidy")
doTidy := func() error {
if config.CertStore {
if err := b.doTidyCertStore(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.RevokedCerts || config.IssuerAssocs {
if err := b.doTidyRevocationStore(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.ExpiredIssuers {
if err := b.doTidyExpiredIssuers(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.BackupBundle {
if err := b.doTidyMoveCABundle(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.RevocationQueue {
if err := b.doTidyRevocationQueue(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.CrossRevokedCerts {
if err := b.doTidyCrossRevocationStore(ctx, req, logger, config); err != nil {
return err
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
if config.TidyAcme {
if err := b.doTidyAcme(ctx, req, logger, config); err != nil {
return err
}
}
return nil
}
if err := doTidy(); err != nil {
logger.Error("error running tidy", "error", err)
b.tidyStatusStop(err)
} else {
b.tidyStatusStop(nil)
// Since the tidy operation finished without an error, we don't
// really want to start another tidy right away (if the interval
// is too short). So mark the last tidy as now.
b.tidyStatusLock.Lock()
b.lastTidy = time.Now()
b.tidyStatusLock.Unlock()
}
}()
}
func (b *backend) doTidyCertStore(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
serials, err := req.Storage.List(ctx, "certs/")
if err != nil {
return fmt.Errorf("error fetching list of certs: %w", err)
}
serialCount := len(serials)
metrics.SetGauge([]string{"secrets", "pki", "tidy", "cert_store_total_entries"}, float32(serialCount))
for i, serial := range serials {
b.tidyStatusMessage(fmt.Sprintf("Tidying certificate store: checking entry %d of %d", i, serialCount))
metrics.SetGauge([]string{"secrets", "pki", "tidy", "cert_store_current_entry"}, float32(i))
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
time.Sleep(config.PauseDuration)
}
certEntry, err := req.Storage.Get(ctx, "certs/"+serial)
if err != nil {
return fmt.Errorf("error fetching certificate %q: %w", serial, err)
}
if certEntry == nil {
logger.Warn("certificate entry is nil; tidying up since it is no longer useful for any server operations", "serial", serial)
if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil {
return fmt.Errorf("error deleting nil entry with serial %s: %w", serial, err)
}
b.tidyStatusIncCertStoreCount()
continue
}
if certEntry.Value == nil || len(certEntry.Value) == 0 {
logger.Warn("certificate entry has no value; tidying up since it is no longer useful for any server operations", "serial", serial)
if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil {
return fmt.Errorf("error deleting entry with nil value with serial %s: %w", serial, err)
}
b.tidyStatusIncCertStoreCount()
continue
}
cert, err := x509.ParseCertificate(certEntry.Value)
if err != nil {
return fmt.Errorf("unable to parse stored certificate with serial %q: %w", serial, err)
}
if time.Since(cert.NotAfter) > config.SafetyBuffer {
if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil {
return fmt.Errorf("error deleting serial %q from storage: %w", serial, err)
}
b.tidyStatusIncCertStoreCount()
}
}
b.tidyStatusLock.RLock()
metrics.SetGauge([]string{"secrets", "pki", "tidy", "cert_store_total_entries_remaining"}, float32(uint(serialCount)-b.tidyStatus.certStoreDeletedCount))
b.tidyStatusLock.RUnlock()
return nil
}
func (b *backend) doTidyRevocationStore(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
// Fetch and parse our issuers so we can associate them if necessary.
sc := b.makeStorageContext(ctx, req.Storage)
issuerIDCertMap, err := fetchIssuerMapForRevocationChecking(sc)
if err != nil {
return err
}
rebuildCRL := false
revokedSerials, err := req.Storage.List(ctx, "revoked/")
if err != nil {
return fmt.Errorf("error fetching list of revoked certs: %w", err)
}
revokedSerialsCount := len(revokedSerials)
metrics.SetGauge([]string{"secrets", "pki", "tidy", "revoked_cert_total_entries"}, float32(revokedSerialsCount))
fixedIssuers := 0
var revInfo revocationInfo
for i, serial := range revokedSerials {
b.tidyStatusMessage(fmt.Sprintf("Tidying revoked certificates: checking certificate %d of %d", i, len(revokedSerials)))
metrics.SetGauge([]string{"secrets", "pki", "tidy", "revoked_cert_current_entry"}, float32(i))
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
b.revokeStorageLock.Unlock()
time.Sleep(config.PauseDuration)
b.revokeStorageLock.Lock()
}
revokedEntry, err := req.Storage.Get(ctx, "revoked/"+serial)
if err != nil {
return fmt.Errorf("unable to fetch revoked cert with serial %q: %w", serial, err)
}
if revokedEntry == nil {
logger.Warn("revoked entry is nil; tidying up since it is no longer useful for any server operations", "serial", serial)
if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil {
return fmt.Errorf("error deleting nil revoked entry with serial %s: %w", serial, err)
}
b.tidyStatusIncRevokedCertCount()
continue
}
if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 {
logger.Warn("revoked entry has nil value; tidying up since it is no longer useful for any server operations", "serial", serial)
if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil {
return fmt.Errorf("error deleting revoked entry with nil value with serial %s: %w", serial, err)
}
b.tidyStatusIncRevokedCertCount()
continue
}
err = revokedEntry.DecodeJSON(&revInfo)
if err != nil {
return fmt.Errorf("error decoding revocation entry for serial %q: %w", serial, err)
}
revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes)
if err != nil {
return fmt.Errorf("unable to parse stored revoked certificate with serial %q: %w", serial, err)
}
// Tidy operations over revoked certs should execute prior to
// tidyRevokedCerts as that may remove the entry. If that happens,
// we won't persist the revInfo changes (as it was deleted instead).
var storeCert bool
if config.IssuerAssocs {
if !isRevInfoIssuerValid(&revInfo, issuerIDCertMap) {
b.tidyStatusIncMissingIssuerCertCount()
revInfo.CertificateIssuer = issuerID("")
storeCert = true
if associateRevokedCertWithIsssuer(&revInfo, revokedCert, issuerIDCertMap) {
fixedIssuers += 1
}
}
}
if config.RevokedCerts {
// Only remove the entries from revoked/ and certs/ if we're
// past its NotAfter value. This is because we use the
// information on revoked/ to build the CRL and the
// information on certs/ for lookup.
if time.Since(revokedCert.NotAfter) > config.SafetyBuffer {
if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil {
return fmt.Errorf("error deleting serial %q from revoked list: %w", serial, err)
}
if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil {
return fmt.Errorf("error deleting serial %q from store when tidying revoked: %w", serial, err)
}
rebuildCRL = true
storeCert = false
b.tidyStatusIncRevokedCertCount()
}
}
// If the entry wasn't removed but was otherwise modified,
// go ahead and write it back out.
if storeCert {
revokedEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo)
if err != nil {
return fmt.Errorf("error building entry to persist changes to serial %v from revoked list: %w", serial, err)
}
err = req.Storage.Put(ctx, revokedEntry)
if err != nil {
return fmt.Errorf("error persisting changes to serial %v from revoked list: %w", serial, err)
}
}
}
b.tidyStatusLock.RLock()
metrics.SetGauge([]string{"secrets", "pki", "tidy", "revoked_cert_total_entries_remaining"}, float32(uint(revokedSerialsCount)-b.tidyStatus.revokedCertDeletedCount))
metrics.SetGauge([]string{"secrets", "pki", "tidy", "revoked_cert_entries_incorrect_issuers"}, float32(b.tidyStatus.missingIssuerCertCount))
metrics.SetGauge([]string{"secrets", "pki", "tidy", "revoked_cert_entries_fixed_issuers"}, float32(fixedIssuers))
b.tidyStatusLock.RUnlock()
if rebuildCRL {
// Expired certificates isn't generally an important
// reason to trigger a CRL rebuild for. Check if
// automatic CRL rebuilds have been enabled and defer
// the rebuild if so.
config, err := sc.getRevocationConfig()
if err != nil {
return err
}
if !config.AutoRebuild {
warnings, err := b.crlBuilder.rebuild(sc, false)
if err != nil {
return err
}
if len(warnings) > 0 {
msg := "During rebuild of CRL for tidy, got the following warnings:"
for index, warning := range warnings {
msg = fmt.Sprintf("%v\n %d. %v", msg, index+1, warning)
}
b.Logger().Warn(msg)
}
}
}
return nil
}
func (b *backend) doTidyExpiredIssuers(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
// We do not support cancelling within the expired issuers operation.
// Any cancellation will occur before or after this operation.
if b.System().ReplicationState().HasState(consts.ReplicationDRSecondary|consts.ReplicationPerformanceStandby) ||
(!b.System().LocalMount() && b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) {
b.Logger().Debug("skipping expired issuer tidy as we're not on the primary or secondary with a local mount")
return nil
}
// Short-circuit to avoid having to deal with the legacy mounts. While we
// could handle this case and remove these issuers, its somewhat
// unexpected behavior and we'd prefer to finish the migration first.
if b.useLegacyBundleCaStorage() {
return nil
}
b.issuersLock.Lock()
defer b.issuersLock.Unlock()
// Fetch and parse our issuers so we have their expiration date.
sc := b.makeStorageContext(ctx, req.Storage)
issuerIDCertMap, err := fetchIssuerMapForRevocationChecking(sc)
if err != nil {
return err
}
// Fetch the issuer config to find the default; we don't want to remove
// the current active issuer automatically.
iConfig, err := sc.getIssuersConfig()
if err != nil {
return err
}
// We want certificates which have expired before this date by a given
// safety buffer.
rebuildChainsAndCRL := false
for issuer, cert := range issuerIDCertMap {
if time.Since(cert.NotAfter) <= config.IssuerSafetyBuffer {
continue
}
entry, err := sc.fetchIssuerById(issuer)
if err != nil {
return nil
}
// This issuer's certificate has expired. We explicitly persist the
// key, but log both the certificate and the keyId to the
// informational logs so an admin can recover the removed cert if
// necessary or remove the key (and know which cert it belonged to),
// if desired.
msg := "[Tidy on mount: %v] Issuer %v has expired by %v and is being removed."
idAndName := fmt.Sprintf("[id:%v/name:%v]", entry.ID, entry.Name)
msg = fmt.Sprintf(msg, b.backendUUID, idAndName, config.IssuerSafetyBuffer)
// Before we log, check if we're the default. While this is late, and
// after we read it from storage, we have more info here to tell the
// user that their default has expired AND has passed the safety
// buffer.
if iConfig.DefaultIssuerId == issuer {
msg = "[Tidy on mount: %v] Issuer %v has expired and would be removed via tidy, but won't be, as it is currently the default issuer."
msg = fmt.Sprintf(msg, b.backendUUID, idAndName)
b.Logger().Warn(msg)
continue
}
// Log the above message..
b.Logger().Info(msg, "serial_number", entry.SerialNumber, "key_id", entry.KeyID, "certificate", entry.Certificate)
wasDefault, err := sc.deleteIssuer(issuer)
if err != nil {
b.Logger().Error(fmt.Sprintf("failed to remove %v: %v", idAndName, err))
return err
}
if wasDefault {
b.Logger().Warn(fmt.Sprintf("expired issuer %v was default; it is strongly encouraged to choose a new default issuer for backwards compatibility", idAndName))
}
rebuildChainsAndCRL = true
}
if rebuildChainsAndCRL {
// When issuers are removed, there's a chance chains change as a
// result; remove them.
if err := sc.rebuildIssuersChains(nil); err != nil {
return err
}
// Removal of issuers is generally a good reason to rebuild the CRL,
// even if auto-rebuild is enabled.
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
warnings, err := b.crlBuilder.rebuild(sc, false)
if err != nil {
return err
}
if len(warnings) > 0 {
msg := "During rebuild of CRL for tidy, got the following warnings:"
for index, warning := range warnings {
msg = fmt.Sprintf("%v\n %d. %v", msg, index+1, warning)
}
b.Logger().Warn(msg)
}
}
return nil
}
func (b *backend) doTidyMoveCABundle(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
// We do not support cancelling within this operation; any cancel will
// occur before or after this operation.
if b.System().ReplicationState().HasState(consts.ReplicationDRSecondary|consts.ReplicationPerformanceStandby) ||
(!b.System().LocalMount() && b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) {
b.Logger().Debug("skipping moving the legacy CA bundle as we're not on the primary or secondary with a local mount")
return nil
}
// Short-circuit to avoid moving the legacy bundle from under a legacy
// mount.
if b.useLegacyBundleCaStorage() {
return nil
}
// If we've already run, exit.
_, bundle, err := getLegacyCertBundle(ctx, req.Storage)
if err != nil {
return fmt.Errorf("failed to fetch the legacy CA bundle: %w", err)
}
if bundle == nil {
b.Logger().Debug("No legacy CA bundle available; nothing to do.")
return nil
}
log, err := getLegacyBundleMigrationLog(ctx, req.Storage)
if err != nil {
return fmt.Errorf("failed to fetch the legacy bundle migration log: %w", err)
}
if log == nil {
return fmt.Errorf("refusing to tidy with an empty legacy migration log but present CA bundle: %w", err)
}
if time.Since(log.Created) <= config.IssuerSafetyBuffer {
b.Logger().Debug("Migration was created too recently to remove the legacy bundle; refusing to move legacy CA bundle to backup location.")
return nil
}
// Do the write before the delete.
entry, err := logical.StorageEntryJSON(legacyCertBundleBackupPath, bundle)
if err != nil {
return fmt.Errorf("failed to create new backup storage entry: %w", err)
}
err = req.Storage.Put(ctx, entry)
if err != nil {
return fmt.Errorf("failed to write new backup legacy CA bundle: %w", err)
}
err = req.Storage.Delete(ctx, legacyCertBundlePath)
if err != nil {
return fmt.Errorf("failed to remove old legacy CA bundle path: %w", err)
}
b.Logger().Info("legacy CA bundle successfully moved to backup location")
return nil
}
func (b *backend) doTidyRevocationQueue(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
if b.System().ReplicationState().HasState(consts.ReplicationDRSecondary|consts.ReplicationPerformanceStandby) ||
(!b.System().LocalMount() && b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) {
b.Logger().Debug("skipping cross-cluster revocation queue tidy as we're not on the primary or secondary with a local mount")
return nil
}
sc := b.makeStorageContext(ctx, req.Storage)
clusters, err := sc.Storage.List(sc.Context, crossRevocationPrefix)
if err != nil {
return fmt.Errorf("failed to list cross-cluster revocation queue participating clusters: %w", err)
}
// Grab locks as we're potentially modifying revocation-related storage.
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
for cIndex, cluster := range clusters {
if cluster[len(cluster)-1] == '/' {
cluster = cluster[0 : len(cluster)-1]
}
cPath := crossRevocationPrefix + cluster + "/"
serials, err := sc.Storage.List(sc.Context, cPath)
if err != nil {
return fmt.Errorf("failed to list cross-cluster revocation queue entries for cluster %v (%v): %w", cluster, cIndex, err)
}
for _, serial := range serials {
// Check for cancellation.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
b.revokeStorageLock.Unlock()
time.Sleep(config.PauseDuration)
b.revokeStorageLock.Lock()
}
// Confirmation entries _should_ be handled by this cluster's
// processRevocationQueue(...) invocation; if not, when the plugin
// reloads, maybeGatherQueueForFirstProcess(...) will remove all
// stale confirmation requests. However, we don't want to force an
// operator to reload their in-use plugin, so allow tidy to also
// clean up confirmation values without reloading.
if serial[len(serial)-1] == '/' {
// Check if we have a confirmed entry.
confirmedPath := cPath + serial + "confirmed"
removalEntry, err := sc.Storage.Get(sc.Context, confirmedPath)
if err != nil {
return fmt.Errorf("error reading revocation confirmation (%v) during tidy: %w", confirmedPath, err)
}
if removalEntry == nil {
continue
}
// Remove potential revocation requests from all clusters.
for _, subCluster := range clusters {
if subCluster[len(subCluster)-1] == '/' {
subCluster = subCluster[0 : len(subCluster)-1]
}
reqPath := subCluster + "/" + serial[0:len(serial)-1]
if err := sc.Storage.Delete(sc.Context, reqPath); err != nil {
return fmt.Errorf("failed to remove confirmed revocation request on candidate cluster (%v): %w", reqPath, err)
}
}
// Then delete the confirmation.
if err := sc.Storage.Delete(sc.Context, confirmedPath); err != nil {
return fmt.Errorf("failed to remove confirmed revocation confirmation (%v): %w", confirmedPath, err)
}
// No need to handle a revocation request at this path: it can't
// still exist on this cluster after we deleted it above.
continue
}
ePath := cPath + serial
entry, err := sc.Storage.Get(sc.Context, ePath)
if err != nil {
return fmt.Errorf("error reading revocation request (%v) to tidy: %w", ePath, err)
}
if entry == nil || entry.Value == nil {
continue
}
var revRequest revocationRequest
if err := entry.DecodeJSON(&revRequest); err != nil {
return fmt.Errorf("error reading revocation request (%v) to tidy: %w", ePath, err)
}
if time.Since(revRequest.RequestedAt) <= config.QueueSafetyBuffer {
continue
}
// Safe to remove this entry.
if err := sc.Storage.Delete(sc.Context, ePath); err != nil {
return fmt.Errorf("error deleting revocation request (%v): %w", ePath, err)
}
// Assumption: there should never be a need to remove this from
// the processing queue on this node. We're on the active primary,
// so our writes don't cause invalidations. This means we'd have
// to have slated it for deletion very quickly after it'd been
// sent (i.e., inside of the 1-minute boundary that periodicFunc
// executes at). While this is possible, because we grab the
// revocationStorageLock above, we can't execute interleaved
// with that periodicFunc, so the periodicFunc would've had to
// finished before we actually did this deletion (or it wouldn't
// have ignored this serial because our deletion would've
// happened prior to it reading the storage entry). Thus we should
// be safe to ignore the revocation queue removal here.
b.tidyStatusIncRevQueueCount()
}
}
return nil
}
func (b *backend) doTidyCrossRevocationStore(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
if b.System().ReplicationState().HasState(consts.ReplicationDRSecondary|consts.ReplicationPerformanceStandby) ||
(!b.System().LocalMount() && b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) {
b.Logger().Debug("skipping cross-cluster revoked certificate store tidy as we're not on the primary or secondary with a local mount")
return nil
}
sc := b.makeStorageContext(ctx, req.Storage)
clusters, err := sc.Storage.List(sc.Context, unifiedRevocationReadPathPrefix)
if err != nil {
return fmt.Errorf("failed to list cross-cluster revoked certificate store participating clusters: %w", err)
}
// Grab locks as we're potentially modifying revocation-related storage.
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
for cIndex, cluster := range clusters {
if cluster[len(cluster)-1] == '/' {
cluster = cluster[0 : len(cluster)-1]
}
cPath := unifiedRevocationReadPathPrefix + cluster + "/"
serials, err := sc.Storage.List(sc.Context, cPath)
if err != nil {
return fmt.Errorf("failed to list cross-cluster revoked certificate store entries for cluster %v (%v): %w", cluster, cIndex, err)
}
for _, serial := range serials {
// Check for cancellation.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
b.revokeStorageLock.Unlock()
time.Sleep(config.PauseDuration)
b.revokeStorageLock.Lock()
}
ePath := cPath + serial
entry, err := sc.Storage.Get(sc.Context, ePath)
if err != nil {
return fmt.Errorf("error reading cross-cluster revocation entry (%v) to tidy: %w", ePath, err)
}
if entry == nil || entry.Value == nil {
continue
}
var details unifiedRevocationEntry
if err := entry.DecodeJSON(&details); err != nil {
return fmt.Errorf("error decoding cross-cluster revocation entry (%v) to tidy: %w", ePath, err)
}
if time.Since(details.CertExpiration) <= config.SafetyBuffer {
continue
}
// Safe to remove this entry.
if err := sc.Storage.Delete(sc.Context, ePath); err != nil {
return fmt.Errorf("error deleting revocation request (%v): %w", ePath, err)
}
b.tidyStatusIncCrossRevCertCount()
}
}
return nil
}
func (b *backend) doTidyAcme(ctx context.Context, req *logical.Request, logger hclog.Logger, config *tidyConfig) error {
b.acmeAccountLock.Lock()
defer b.acmeAccountLock.Unlock()
sc := b.makeStorageContext(ctx, req.Storage)
thumbprints, err := sc.Storage.List(ctx, acmeThumbprintPrefix)
if err != nil {
return err
}
b.tidyStatusLock.Lock()
b.tidyStatus.acmeAccountsCount = uint(len(thumbprints))
b.tidyStatusLock.Unlock()
baseUrl, _, err := getAcmeBaseUrl(sc, req)
if err != nil {
return err
}
acmeCtx := &acmeContext{
baseUrl: baseUrl,
sc: sc,
}
for _, thumbprint := range thumbprints {
err := b.tidyAcmeAccountByThumbprint(b.acmeState, acmeCtx, thumbprint, config.SafetyBuffer, config.AcmeAccountSafetyBuffer)
if err != nil {
logger.Warn("error tidying account %v: %v", thumbprint, err.Error())
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
b.acmeAccountLock.Unlock() // Correct the Lock
time.Sleep(config.PauseDuration)
b.acmeAccountLock.Lock()
}
}
// Clean up any unused EAB
eabIds, err := b.acmeState.ListEabIds(sc)
if err != nil {
return fmt.Errorf("failed listing EAB ids: %w", err)
}
for _, eabId := range eabIds {
eab, err := b.acmeState.LoadEab(sc, eabId)
if err != nil {
if errors.Is(err, ErrStorageItemNotFound) {
// We don't need to worry about a consumed EAB
continue
}
return err
}
eabExpiration := eab.CreatedOn.Add(config.AcmeAccountSafetyBuffer)
if time.Now().After(eabExpiration) {
_, err := b.acmeState.DeleteEab(sc, eabId)
if err != nil {
return fmt.Errorf("failed to tidy eab %s: %w", eabId, err)
}
}
// Check for cancel before continuing.
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 1, 0) {
return tidyCancelledError
}
// Check for pause duration to reduce resource consumption.
if config.PauseDuration > (0 * time.Second) {
b.acmeAccountLock.Unlock() // Correct the Lock
time.Sleep(config.PauseDuration)
b.acmeAccountLock.Lock()
}
}
return nil
}
func (b *backend) pathTidyCancelWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
if atomic.LoadUint32(b.tidyCASGuard) == 0 {
resp := &logical.Response{}
resp.AddWarning("Tidy operation cannot be cancelled as none is currently running.")
return resp, nil
}
// Grab the status lock before writing the cancel atomic. This lets us
// update the status correctly as well, avoiding writing it if we're not
// presently running.
//
// Unlock needs to occur prior to calling read.
b.tidyStatusLock.Lock()
if b.tidyStatus.state == tidyStatusStarted || atomic.LoadUint32(b.tidyCASGuard) == 1 {
if atomic.CompareAndSwapUint32(b.tidyCancelCAS, 0, 1) {
b.tidyStatus.state = tidyStatusCancelling
}
}
b.tidyStatusLock.Unlock()
return b.pathTidyStatusRead(ctx, req, d)
}
func (b *backend) pathTidyStatusRead(_ context.Context, _ *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
b.tidyStatusLock.RLock()
defer b.tidyStatusLock.RUnlock()
resp := &logical.Response{
Data: map[string]interface{}{
"safety_buffer": nil,
"issuer_safety_buffer": nil,
"tidy_cert_store": nil,
"tidy_revoked_certs": nil,
"tidy_revoked_cert_issuer_associations": nil,
"tidy_expired_issuers": nil,
"tidy_move_legacy_ca_bundle": nil,
"tidy_revocation_queue": nil,
"tidy_cross_cluster_revoked_certs": nil,
"tidy_acme": nil,
"pause_duration": nil,
"state": "Inactive",
"error": nil,
"time_started": nil,
"time_finished": nil,
"message": nil,
"cert_store_deleted_count": nil,
"revoked_cert_deleted_count": nil,
"missing_issuer_cert_count": nil,
"current_cert_store_count": nil,
"current_revoked_cert_count": nil,
"internal_backend_uuid": nil,
"revocation_queue_deleted_count": nil,
"cross_revoked_cert_deleted_count": nil,
"total_acme_account_count": nil,
"acme_account_deleted_count": nil,
"acme_account_revoked_count": nil,
"acme_orders_deleted_count": nil,
"acme_account_safety_buffer": nil,
},
}
resp.Data["internal_backend_uuid"] = b.backendUUID
if b.certCountEnabled.Load() {
resp.Data["current_cert_store_count"] = b.certCount.Load()
resp.Data["current_revoked_cert_count"] = b.revokedCertCount.Load()
if !b.certsCounted.Load() {
resp.AddWarning("Certificates in storage are still being counted, current counts provided may be " +
"inaccurate")
}
if b.certCountError != "" {
resp.Data["certificate_counting_error"] = b.certCountError
}
}
if b.tidyStatus.state == tidyStatusInactive {
return resp, nil
}
resp.Data["safety_buffer"] = b.tidyStatus.safetyBuffer
resp.Data["issuer_safety_buffer"] = b.tidyStatus.issuerSafetyBuffer
resp.Data["tidy_cert_store"] = b.tidyStatus.tidyCertStore
resp.Data["tidy_revoked_certs"] = b.tidyStatus.tidyRevokedCerts
resp.Data["tidy_revoked_cert_issuer_associations"] = b.tidyStatus.tidyRevokedAssocs
resp.Data["tidy_expired_issuers"] = b.tidyStatus.tidyExpiredIssuers
resp.Data["tidy_move_legacy_ca_bundle"] = b.tidyStatus.tidyBackupBundle
resp.Data["tidy_revocation_queue"] = b.tidyStatus.tidyRevocationQueue
resp.Data["tidy_cross_cluster_revoked_certs"] = b.tidyStatus.tidyCrossRevokedCerts
resp.Data["tidy_acme"] = b.tidyStatus.tidyAcme
resp.Data["pause_duration"] = b.tidyStatus.pauseDuration
resp.Data["time_started"] = b.tidyStatus.timeStarted
resp.Data["message"] = b.tidyStatus.message
resp.Data["cert_store_deleted_count"] = b.tidyStatus.certStoreDeletedCount
resp.Data["revoked_cert_deleted_count"] = b.tidyStatus.revokedCertDeletedCount
resp.Data["missing_issuer_cert_count"] = b.tidyStatus.missingIssuerCertCount
resp.Data["revocation_queue_deleted_count"] = b.tidyStatus.revQueueDeletedCount
resp.Data["cross_revoked_cert_deleted_count"] = b.tidyStatus.crossRevokedDeletedCount
resp.Data["revocation_queue_safety_buffer"] = b.tidyStatus.revQueueSafetyBuffer
resp.Data["last_auto_tidy_finished"] = b.lastTidy
resp.Data["total_acme_account_count"] = b.tidyStatus.acmeAccountsCount
resp.Data["acme_account_deleted_count"] = b.tidyStatus.acmeAccountsDeletedCount
resp.Data["acme_account_revoked_count"] = b.tidyStatus.acmeAccountsRevokedCount
resp.Data["acme_orders_deleted_count"] = b.tidyStatus.acmeOrdersDeletedCount
resp.Data["acme_account_safety_buffer"] = b.tidyStatus.acmeAccountSafetyBuffer
switch b.tidyStatus.state {
case tidyStatusStarted:
resp.Data["state"] = "Running"
case tidyStatusFinished:
resp.Data["state"] = "Finished"
resp.Data["time_finished"] = b.tidyStatus.timeFinished
resp.Data["message"] = nil
case tidyStatusError:
resp.Data["state"] = "Error"
resp.Data["time_finished"] = b.tidyStatus.timeFinished
resp.Data["error"] = b.tidyStatus.err.Error()
// Don't clear the message so that it serves as a hint about when
// the error occurred.
case tidyStatusCancelling:
resp.Data["state"] = "Cancelling"
case tidyStatusCancelled:
resp.Data["state"] = "Cancelled"
resp.Data["time_finished"] = b.tidyStatus.timeFinished
}
return resp, nil
}
func (b *backend) pathConfigAutoTidyRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
sc := b.makeStorageContext(ctx, req.Storage)
config, err := sc.getAutoTidyConfig()
if err != nil {
return nil, err
}
return &logical.Response{
Data: getTidyConfigData(*config),
}, nil
}
func (b *backend) pathConfigAutoTidyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
sc := b.makeStorageContext(ctx, req.Storage)
config, err := sc.getAutoTidyConfig()
if err != nil {
return nil, err
}
if enabledRaw, ok := d.GetOk("enabled"); ok {
config.Enabled = enabledRaw.(bool)
}
if intervalRaw, ok := d.GetOk("interval_duration"); ok {
config.Interval = time.Duration(intervalRaw.(int)) * time.Second
if config.Interval < 0 {
return logical.ErrorResponse(fmt.Sprintf("given interval_duration must be greater than or equal to zero seconds; got: %v", intervalRaw)), nil
}
}
if certStoreRaw, ok := d.GetOk("tidy_cert_store"); ok {
config.CertStore = certStoreRaw.(bool)
}
if revokedCertsRaw, ok := d.GetOk("tidy_revoked_certs"); ok {
config.RevokedCerts = revokedCertsRaw.(bool)
}
if issuerAssocRaw, ok := d.GetOk("tidy_revoked_cert_issuer_associations"); ok {
config.IssuerAssocs = issuerAssocRaw.(bool)
}
if safetyBufferRaw, ok := d.GetOk("safety_buffer"); ok {
config.SafetyBuffer = time.Duration(safetyBufferRaw.(int)) * time.Second
if config.SafetyBuffer < 1*time.Second {
return logical.ErrorResponse(fmt.Sprintf("given safety_buffer must be at least one second; got: %v", safetyBufferRaw)), nil
}
}
if pauseDurationRaw, ok := d.GetOk("pause_duration"); ok {
config.PauseDuration, err = parseutil.ParseDurationSecond(pauseDurationRaw.(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("unable to parse given pause_duration: %v", err)), nil
}
if config.PauseDuration < (0 * time.Second) {
return logical.ErrorResponse("received invalid, negative pause_duration"), nil
}
}
if expiredIssuers, ok := d.GetOk("tidy_expired_issuers"); ok {
config.ExpiredIssuers = expiredIssuers.(bool)
}
if issuerSafetyBufferRaw, ok := d.GetOk("issuer_safety_buffer"); ok {
config.IssuerSafetyBuffer = time.Duration(issuerSafetyBufferRaw.(int)) * time.Second
if config.IssuerSafetyBuffer < 1*time.Second {
return logical.ErrorResponse(fmt.Sprintf("given safety_buffer must be at least one second; got: %v", issuerSafetyBufferRaw)), nil
}
}
if backupBundle, ok := d.GetOk("tidy_move_legacy_ca_bundle"); ok {
config.BackupBundle = backupBundle.(bool)
}
if revocationQueueRaw, ok := d.GetOk("tidy_revocation_queue"); ok {
config.RevocationQueue = revocationQueueRaw.(bool)
}
if queueSafetyBufferRaw, ok := d.GetOk("revocation_queue_safety_buffer"); ok {
config.QueueSafetyBuffer = time.Duration(queueSafetyBufferRaw.(int)) * time.Second
if config.QueueSafetyBuffer < 1*time.Second {
return logical.ErrorResponse(fmt.Sprintf("given revocation_queue_safety_buffer must be at least one second; got: %v", queueSafetyBufferRaw)), nil
}
}
if crossRevokedRaw, ok := d.GetOk("tidy_cross_cluster_revoked_certs"); ok {
config.CrossRevokedCerts = crossRevokedRaw.(bool)
}
if tidyAcmeRaw, ok := d.GetOk("tidy_acme"); ok {
config.TidyAcme = tidyAcmeRaw.(bool)
}
if config.Enabled && !config.IsAnyTidyEnabled() {
return logical.ErrorResponse("Auto-tidy enabled but no tidy operations were requested. Enable at least one tidy operation to be run (" + config.AnyTidyConfig() + ")."), nil
}
if maintainCountEnabledRaw, ok := d.GetOk("maintain_stored_certificate_counts"); ok {
config.MaintainCount = maintainCountEnabledRaw.(bool)
}
if runningStorageMetricsEnabledRaw, ok := d.GetOk("publish_stored_certificate_count_metrics"); ok {
config.PublishMetrics = runningStorageMetricsEnabledRaw.(bool)
}
if config.PublishMetrics && !config.MaintainCount {
return logical.ErrorResponse("Can not publish a running storage metrics count to metrics without first maintaining that count. Enable `maintain_stored_certificate_counts` to enable `publish_stored_certificate_count_metrics`."), nil
}
if err := sc.writeAutoTidyConfig(config); err != nil {
return nil, err
}
return &logical.Response{
Data: getTidyConfigData(*config),
}, nil
}
func (b *backend) tidyStatusStart(config *tidyConfig) {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus = &tidyStatus{
safetyBuffer: int(config.SafetyBuffer / time.Second),
issuerSafetyBuffer: int(config.IssuerSafetyBuffer / time.Second),
revQueueSafetyBuffer: int(config.QueueSafetyBuffer / time.Second),
acmeAccountSafetyBuffer: int(config.AcmeAccountSafetyBuffer / time.Second),
tidyCertStore: config.CertStore,
tidyRevokedCerts: config.RevokedCerts,
tidyRevokedAssocs: config.IssuerAssocs,
tidyExpiredIssuers: config.ExpiredIssuers,
tidyBackupBundle: config.BackupBundle,
tidyRevocationQueue: config.RevocationQueue,
tidyCrossRevokedCerts: config.CrossRevokedCerts,
tidyAcme: config.TidyAcme,
pauseDuration: config.PauseDuration.String(),
state: tidyStatusStarted,
timeStarted: time.Now(),
}
metrics.SetGauge([]string{"secrets", "pki", "tidy", "start_time_epoch"}, float32(b.tidyStatus.timeStarted.Unix()))
}
func (b *backend) tidyStatusStop(err error) {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.timeFinished = time.Now()
b.tidyStatus.err = err
if err == nil {
b.tidyStatus.state = tidyStatusFinished
} else if err == tidyCancelledError {
b.tidyStatus.state = tidyStatusCancelled
} else {
b.tidyStatus.state = tidyStatusError
}
metrics.MeasureSince([]string{"secrets", "pki", "tidy", "duration"}, b.tidyStatus.timeStarted)
metrics.SetGauge([]string{"secrets", "pki", "tidy", "start_time_epoch"}, 0)
metrics.IncrCounter([]string{"secrets", "pki", "tidy", "cert_store_deleted_count"}, float32(b.tidyStatus.certStoreDeletedCount))
metrics.IncrCounter([]string{"secrets", "pki", "tidy", "revoked_cert_deleted_count"}, float32(b.tidyStatus.revokedCertDeletedCount))
if err != nil {
metrics.IncrCounter([]string{"secrets", "pki", "tidy", "failure"}, 1)
} else {
metrics.IncrCounter([]string{"secrets", "pki", "tidy", "success"}, 1)
}
}
func (b *backend) tidyStatusMessage(msg string) {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.message = msg
}
func (b *backend) tidyStatusIncCertStoreCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.certStoreDeletedCount++
b.ifCountEnabledDecrementTotalCertificatesCountReport()
}
func (b *backend) tidyStatusIncRevokedCertCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.revokedCertDeletedCount++
b.ifCountEnabledDecrementTotalRevokedCertificatesCountReport()
}
func (b *backend) tidyStatusIncMissingIssuerCertCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.missingIssuerCertCount++
}
func (b *backend) tidyStatusIncRevQueueCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.revQueueDeletedCount++
}
func (b *backend) tidyStatusIncCrossRevCertCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.crossRevokedDeletedCount++
}
func (b *backend) tidyStatusIncRevAcmeAccountCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.acmeAccountsRevokedCount++
}
func (b *backend) tidyStatusIncDeletedAcmeAccountCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.acmeAccountsDeletedCount++
}
func (b *backend) tidyStatusIncDelAcmeOrderCount() {
b.tidyStatusLock.Lock()
defer b.tidyStatusLock.Unlock()
b.tidyStatus.acmeOrdersDeletedCount++
}
const pathTidyHelpSyn = `
Tidy up the backend by removing expired certificates, revocation information,
or both.
`
const pathTidyHelpDesc = `
This endpoint allows expired certificates and/or revocation information to be
removed from the backend, freeing up storage and shortening CRLs.
For safety, this function is a noop if called without parameters; cleanup from
normal certificate storage must be enabled with 'tidy_cert_store' and cleanup
from revocation information must be enabled with 'tidy_revocation_list'.
The 'safety_buffer' parameter is useful to ensure that clock skew amongst your
hosts cannot lead to a certificate being removed from the CRL while it is still
considered valid by other hosts (for instance, if their clocks are a few
minutes behind). The 'safety_buffer' parameter can be an integer number of
seconds or a string duration like "72h".
All certificates and/or revocation information currently stored in the backend
will be checked when this endpoint is hit. The expiration of the
certificate/revocation information of each certificate being held in
certificate storage or in revocation information will then be checked. If the
current time, minus the value of 'safety_buffer', is greater than the
expiration, it will be removed.
`
const pathTidyCancelHelpSyn = `
Cancels a currently running tidy operation.
`
const pathTidyCancelHelpDesc = `
This endpoint allows cancelling a currently running tidy operation.
Periodically throughout the invocation of tidy, we'll check if the operation
has been requested to be cancelled. If so, we'll stop the currently running
tidy operation.
`
const pathTidyStatusHelpSyn = `
Returns the status of the tidy operation.
`
const pathTidyStatusHelpDesc = `
This is a read only endpoint that returns information about the current tidy
operation, or the most recent if none is currently running.
The result includes the following fields:
* 'safety_buffer': the value of this parameter when initiating the tidy operation
* 'tidy_cert_store': the value of this parameter when initiating the tidy operation
* 'tidy_revoked_certs': the value of this parameter when initiating the tidy operation
* 'tidy_revoked_cert_issuer_associations': the value of this parameter when initiating the tidy operation
* 'state': one of "Inactive", "Running", "Finished", "Error"
* 'error': the error message, if the operation ran into an error
* 'time_started': the time the operation started
* 'time_finished': the time the operation finished
* 'message': One of "Tidying certificate store: checking entry N of TOTAL" or
"Tidying revoked certificates: checking certificate N of TOTAL"
* 'cert_store_deleted_count': The number of certificate storage entries deleted
* 'revoked_cert_deleted_count': The number of revoked certificate entries deleted
* 'missing_issuer_cert_count': The number of revoked certificates which were missing a valid issuer reference
* 'tidy_expired_issuers': the value of this parameter when initiating the tidy operation
* 'issuer_safety_buffer': the value of this parameter when initiating the tidy operation
* 'tidy_move_legacy_ca_bundle': the value of this parameter when initiating the tidy operation
* 'tidy_revocation_queue': the value of this parameter when initiating the tidy operation
* 'revocation_queue_deleted_count': the number of revocation queue entries deleted
* 'tidy_cross_cluster_revoked_certs': the value of this parameter when initiating the tidy operation
* 'cross_revoked_cert_deleted_count': the number of cross-cluster revoked certificate entries deleted
* 'revocation_queue_safety_buffer': the value of this parameter when initiating the tidy operation
* 'tidy_acme': the value of this parameter when initiating the tidy operation
* 'acme_account_safety_buffer': the value of this parameter when initiating the tidy operation
* 'total_acme_account_count': the total number of acme accounts in the list to be iterated over
* 'acme_account_deleted_count': the number of revoked acme accounts deleted during the operation
* 'acme_account_revoked_count': the number of acme accounts revoked during the operation
* 'acme_orders_deleted_count': the number of acme orders deleted during the operation
`
const pathConfigAutoTidySyn = `
Modifies the current configuration for automatic tidy execution.
`
const pathConfigAutoTidyDesc = `
This endpoint accepts parameters to a tidy operation (see /tidy) that
will be used for automatic tidy execution. This takes two extra parameters,
enabled (to enable or disable auto-tidy) and interval_duration (which
controls the frequency of auto-tidy execution).
Once enabled, a tidy operation will be kicked off automatically, as if it
were executed with the posted configuration.
`
func getTidyConfigData(config tidyConfig) map[string]interface{} {
return map[string]interface{}{
// This map is in the same order as tidyConfig to ensure that all fields are accounted for
"enabled": config.Enabled,
"interval_duration": int(config.Interval / time.Second),
"tidy_cert_store": config.CertStore,
"tidy_revoked_certs": config.RevokedCerts,
"tidy_revoked_cert_issuer_associations": config.IssuerAssocs,
"tidy_expired_issuers": config.ExpiredIssuers,
"tidy_move_legacy_ca_bundle": config.BackupBundle,
"tidy_acme": config.TidyAcme,
"safety_buffer": int(config.SafetyBuffer / time.Second),
"issuer_safety_buffer": int(config.IssuerSafetyBuffer / time.Second),
"acme_account_safety_buffer": int(config.AcmeAccountSafetyBuffer / time.Second),
"pause_duration": config.PauseDuration.String(),
"publish_stored_certificate_count_metrics": config.PublishMetrics,
"maintain_stored_certificate_counts": config.MaintainCount,
"tidy_revocation_queue": config.RevocationQueue,
"revocation_queue_safety_buffer": int(config.QueueSafetyBuffer / time.Second),
"tidy_cross_cluster_revoked_certs": config.CrossRevokedCerts,
}
}