// 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, } }