mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
PKI: Track last time auto tidy was run across restarts (#28488)
* Track the last PKI auto-tidy time ran for use across nodes - If the interval time for auto-tidy is longer then say a regularly scheduled restart of Vault, auto-tidy is never run. This is due to the time of the last run of tidy is only kept in memory and initialized on startup to the current time - Store the last run of any tidy, to maintain previous behavior, to a cluster local file, which is read in/initialized upon a mount initialization. * Add auto-tidy configuration fields for backing off at startup * Add new auto-tidy fields to UI * Update api docs for auto-tidy * Add cl * Update field description text * Apply Claire's suggestions from code review Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * Implementing PR feedback from the UI team * remove explicit defaults and types so we retrieve from backend, decouple enabling auto tidy from duration, move params to auto settings section --------- Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
This commit is contained in:
parent
31d58145fd
commit
2db2a9fb5d
16 changed files with 570 additions and 289 deletions
|
|
@ -131,6 +131,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
issuing.PathCerts,
|
||||
issuing.PathCertMetadata,
|
||||
acmePathPrefix,
|
||||
autoTidyLastRunPath,
|
||||
},
|
||||
|
||||
Root: []string{
|
||||
|
|
@ -294,8 +295,12 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
conf.System.ReplicationState().HasState(consts.ReplicationDRSecondary)
|
||||
b.crlBuilder = newCRLBuilder(!cannotRebuildCRLs)
|
||||
|
||||
// Delay the first tidy until after we've started up.
|
||||
b.lastTidy = time.Now()
|
||||
// Delay the first tidy until after we've started up, this will be reset within the initialize function
|
||||
now := time.Now()
|
||||
b.lastAutoTidy = now
|
||||
|
||||
// Keep track of when this mount was started up.
|
||||
b.mountStartup = now
|
||||
|
||||
b.unifiedTransferStatus = newUnifiedTransferStatus()
|
||||
|
||||
|
|
@ -320,7 +325,14 @@ type backend struct {
|
|||
|
||||
tidyStatusLock sync.RWMutex
|
||||
tidyStatus *tidyStatus
|
||||
lastTidy time.Time
|
||||
// lastAutoTidy should be accessed through the tidyStatusLock,
|
||||
// use getAutoTidyLastRun and writeAutoTidyLastRun instead of direct access
|
||||
lastAutoTidy time.Time
|
||||
|
||||
// autoTidyBackoff a random time in the future in which auto-tidy can't start
|
||||
// for after the system starts up to avoid a thundering herd of tidy operations
|
||||
// at startup.
|
||||
autoTidyBackoff time.Time
|
||||
|
||||
unifiedTransferStatus *UnifiedTransferStatus
|
||||
|
||||
|
|
@ -335,6 +347,9 @@ type backend struct {
|
|||
// Context around ACME operations
|
||||
acmeState *acmeState
|
||||
acmeAccountLock sync.RWMutex // (Write) Locked on Tidy, (Read) Locked on Account Creation
|
||||
|
||||
// Track when this mount was started.
|
||||
mountStartup time.Time
|
||||
}
|
||||
|
||||
// BackendOps a bridge/legacy interface until we can further
|
||||
|
|
@ -448,9 +463,38 @@ func (b *backend) initialize(ctx context.Context, ir *logical.InitializationRequ
|
|||
b.GetCertificateCounter().SetError(err)
|
||||
}
|
||||
|
||||
// Initialize lastAutoTidy from disk
|
||||
b.initializeLastTidyFromStorage(sc)
|
||||
|
||||
return b.initializeEnt(sc, ir)
|
||||
}
|
||||
|
||||
// initializeLastTidyFromStorage reads the time we last ran auto tidy from storage and initializes
|
||||
// b.lastAutoTidy with the value. If no previous value existed, we persist time.Now() and initialize
|
||||
// b.lastAutoTidy with that value.
|
||||
func (b *backend) initializeLastTidyFromStorage(sc *storageContext) {
|
||||
now := time.Now()
|
||||
|
||||
lastTidyTime, err := sc.getAutoTidyLastRun()
|
||||
if err != nil {
|
||||
lastTidyTime = now
|
||||
b.Logger().Error("failed loading previous tidy last run time, using now", "error", err.Error())
|
||||
}
|
||||
if lastTidyTime.IsZero() {
|
||||
// No previous time was set, persist now so we can track a starting point across Vault restarts
|
||||
lastTidyTime = now
|
||||
if err = b.updateLastAutoTidyTime(sc, now); err != nil {
|
||||
b.Logger().Error("failed persisting tidy last run time", "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// We bypass using updateLastAutoTidyTime here to avoid the storage write on init
|
||||
// that normally isn't required
|
||||
b.tidyStatusLock.Lock()
|
||||
defer b.tidyStatusLock.Unlock()
|
||||
b.lastAutoTidy = lastTidyTime
|
||||
}
|
||||
|
||||
func (b *backend) cleanup(ctx context.Context) {
|
||||
sc := b.makeStorageContext(ctx, b.storage)
|
||||
|
||||
|
|
@ -708,13 +752,20 @@ func (b *backend) periodicFunc(ctx context.Context, request *logical.Request) er
|
|||
|
||||
// Check if we should run another tidy...
|
||||
now := time.Now()
|
||||
b.tidyStatusLock.RLock()
|
||||
nextOp := b.lastTidy.Add(config.Interval)
|
||||
b.tidyStatusLock.RUnlock()
|
||||
nextOp := b.getLastAutoTidyTime().Add(config.Interval)
|
||||
if now.Before(nextOp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.autoTidyBackoff.IsZero() {
|
||||
b.autoTidyBackoff = config.CalculateStartupBackoff(b.mountStartup)
|
||||
}
|
||||
|
||||
if b.autoTidyBackoff.After(now) {
|
||||
b.Logger().Info("Auto tidy will not run as we are still within the random backoff ending at", "backoff_until", b.autoTidyBackoff)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure a tidy isn't already running... If it is, we'll trigger
|
||||
// again when the running one finishes.
|
||||
if !atomic.CompareAndSwapUint32(b.tidyCASGuard, 0, 1) {
|
||||
|
|
@ -724,9 +775,11 @@ func (b *backend) periodicFunc(ctx context.Context, request *logical.Request) er
|
|||
// Prevent ourselves from starting another tidy operation while
|
||||
// this one is still running. This operation runs in the background
|
||||
// and has a separate error reporting mechanism.
|
||||
b.tidyStatusLock.Lock()
|
||||
b.lastTidy = now
|
||||
b.tidyStatusLock.Unlock()
|
||||
err = b.updateLastAutoTidyTime(sc, now)
|
||||
if err != nil {
|
||||
// We don't really mind if this write fails, we'll re-run in the future
|
||||
b.Logger().Warn("failed to persist auto tidy last run time", "error", err.Error())
|
||||
}
|
||||
|
||||
// Because the request from the parent storage will be cleared at
|
||||
// some point (and potentially reused) -- due to tidy executing in
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
|
@ -81,8 +82,10 @@ type tidyStatus struct {
|
|||
|
||||
type tidyConfig struct {
|
||||
// AutoTidy config
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval time.Duration `json:"interval_duration"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval time.Duration `json:"interval_duration"`
|
||||
MinStartupBackoff time.Duration `json:"min_startup_backoff_duration"`
|
||||
MaxStartupBackoff time.Duration `json:"max_startup_backoff_duration"`
|
||||
|
||||
// Tidy Operations
|
||||
CertStore bool `json:"tidy_cert_store"`
|
||||
|
|
@ -116,9 +119,24 @@ 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"
|
||||
}
|
||||
|
||||
func (tc *tidyConfig) CalculateStartupBackoff(mountStartup time.Time) time.Time {
|
||||
minBackoff := int64(tc.MinStartupBackoff.Seconds())
|
||||
maxBackoff := int64(tc.MaxStartupBackoff.Seconds())
|
||||
|
||||
maxNumber := maxBackoff - minBackoff
|
||||
if maxNumber <= 0 {
|
||||
return mountStartup.Add(tc.MinStartupBackoff)
|
||||
}
|
||||
|
||||
backoffSecs := rand.Int64N(maxNumber) + minBackoff
|
||||
return mountStartup.Add(time.Duration(backoffSecs) * time.Second)
|
||||
}
|
||||
|
||||
var defaultTidyConfig = tidyConfig{
|
||||
Enabled: false,
|
||||
Interval: 12 * time.Hour,
|
||||
MinStartupBackoff: 5 * time.Minute,
|
||||
MaxStartupBackoff: 15 * time.Minute,
|
||||
CertStore: false,
|
||||
RevokedCerts: false,
|
||||
IssuerAssocs: false,
|
||||
|
|
@ -561,6 +579,108 @@ func pathTidyStatus(b *backend) *framework.Path {
|
|||
}
|
||||
|
||||
func pathConfigAutoTidy(b *backend) *framework.Path {
|
||||
autoTidyResponseFields := map[string]*framework.FieldSchema{
|
||||
"enabled": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Specifies whether automatic tidy is enabled or not`,
|
||||
Required: true,
|
||||
},
|
||||
"min_startup_backoff_duration": {
|
||||
Type: framework.TypeInt,
|
||||
Description: `The minimum amount of time in seconds auto-tidy will be delayed after startup`,
|
||||
Required: true,
|
||||
},
|
||||
"max_startup_backoff_duration": {
|
||||
Type: framework.TypeInt,
|
||||
Description: `The maximum amount of time in seconds auto-tidy will be delayed after startup`,
|
||||
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,
|
||||
},
|
||||
"tidy_cert_metadata": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy cert metadata`,
|
||||
Required: true,
|
||||
},
|
||||
"tidy_cmpv2_nonce_store": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy CMPv2 nonce store`,
|
||||
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,
|
||||
},
|
||||
}
|
||||
return &framework.Path{
|
||||
Pattern: "config/auto-tidy",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
|
|
@ -571,6 +691,16 @@ func pathConfigAutoTidy(b *backend) *framework.Path {
|
|||
Type: framework.TypeBool,
|
||||
Description: `Set to true to enable automatic tidy operations.`,
|
||||
},
|
||||
"min_startup_backoff_duration": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: `The minimum amount of time in seconds auto-tidy will be delayed after startup.`,
|
||||
Default: int(defaultTidyConfig.MinStartupBackoff.Seconds()),
|
||||
},
|
||||
"max_startup_backoff_duration": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: `The maximum amount of time in seconds auto-tidy will be delayed after startup.`,
|
||||
Default: int(defaultTidyConfig.MaxStartupBackoff.Seconds()),
|
||||
},
|
||||
"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.`,
|
||||
|
|
@ -601,97 +731,7 @@ available on the tidy-status endpoint.`,
|
|||
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,
|
||||
},
|
||||
"tidy_cert_metadata": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy cert metadata`,
|
||||
Required: true,
|
||||
},
|
||||
"tidy_cmpv2_nonce_store": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy CMPv2 nonce store`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
Fields: autoTidyResponseFields,
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
|
@ -704,98 +744,7 @@ available on the tidy-status endpoint.`,
|
|||
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,
|
||||
},
|
||||
"tidy_cert_metadata": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy cert metadata`,
|
||||
Required: true,
|
||||
},
|
||||
"tidy_cmpv2_nonce_store": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Tidy CMPv2 nonce store`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
Fields: autoTidyResponseFields,
|
||||
}},
|
||||
},
|
||||
// Read more about why these flags are set in backend.go.
|
||||
|
|
@ -896,16 +845,20 @@ func (b *backend) pathTidyWrite(ctx context.Context, req *logical.Request, d *fr
|
|||
Storage: req.Storage,
|
||||
}
|
||||
|
||||
resp := &logical.Response{}
|
||||
// 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()
|
||||
// NOTE: not sure this is correct as we are updating the auto tidy time with this manual run. Ideally we
|
||||
// could track when we ran each type of tidy was last run which would allow manual runs and auto
|
||||
// runs to properly impact each other.
|
||||
sc := b.makeStorageContext(ctx, req.Storage)
|
||||
if err := b.updateLastAutoTidyTime(sc, time.Now()); err != nil {
|
||||
resp.AddWarning(fmt.Sprintf("failed persisting tidy last run time: %v", err))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
@ -1042,9 +995,10 @@ func (b *backend) startTidyOperation(req *logical.Request, config *tidyConfig) {
|
|||
// 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()
|
||||
sc := b.makeStorageContext(ctx, req.Storage)
|
||||
if err := b.updateLastAutoTidyTime(sc, time.Now()); err != nil {
|
||||
logger.Error("error persisting last tidy run time", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1770,6 +1724,7 @@ func (b *backend) pathTidyStatusRead(_ context.Context, _ *logical.Request, _ *f
|
|||
"acme_account_safety_buffer": nil,
|
||||
"cert_metadata_deleted_count": nil,
|
||||
"cmpv2_nonce_deleted_count": nil,
|
||||
"last_auto_tidy_finished": b.getLastAutoTidyTime(),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1814,7 +1769,6 @@ func (b *backend) pathTidyStatusRead(_ context.Context, _ *logical.Request, _ *f
|
|||
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
|
||||
|
|
@ -1865,8 +1819,44 @@ func (b *backend) pathConfigAutoTidyWrite(ctx context.Context, req *logical.Requ
|
|||
return nil, err
|
||||
}
|
||||
|
||||
isAutoTidyBeingEnabled := false
|
||||
|
||||
if enabledRaw, ok := d.GetOk("enabled"); ok {
|
||||
config.Enabled = enabledRaw.(bool)
|
||||
enabled, err := parseutil.ParseBool(enabledRaw)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("failed to parse enabled flag as a boolean: %s", err.Error())), nil
|
||||
}
|
||||
if !config.Enabled && enabled {
|
||||
// we are turning on auto-tidy reset our persisted time to now
|
||||
isAutoTidyBeingEnabled = true
|
||||
}
|
||||
config.Enabled = enabled
|
||||
}
|
||||
|
||||
if minStartupBackoffRaw, ok := d.GetOk("min_startup_backoff_duration"); ok {
|
||||
minDuration, err := parseutil.ParseDurationSecond(minStartupBackoffRaw)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("failed to parse min_startup_backoff_duration flag as a duration: %s", err.Error())), nil
|
||||
}
|
||||
if minDuration.Seconds() < 1 {
|
||||
return logical.ErrorResponse(fmt.Sprintf("min_startup_backoff_duration must be at least 1 second: parsed: %v", minDuration)), nil
|
||||
}
|
||||
config.MinStartupBackoff = minDuration
|
||||
}
|
||||
|
||||
if maxStartupBackoffRaw, ok := d.GetOk("max_startup_backoff_duration"); ok {
|
||||
maxDuration, err := parseutil.ParseDurationSecond(maxStartupBackoffRaw)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("failed to parse max_startup_backoff_duration flag as a duration: %s", err.Error())), nil
|
||||
}
|
||||
if maxDuration.Seconds() < 1 {
|
||||
return logical.ErrorResponse(fmt.Sprintf("max_startup_backoff_duration must be at least 1 second: parsed: %v", maxDuration)), nil
|
||||
}
|
||||
config.MaxStartupBackoff = maxDuration
|
||||
}
|
||||
|
||||
if config.MinStartupBackoff > config.MaxStartupBackoff {
|
||||
return logical.ErrorResponse(fmt.Sprintf("max_startup_backoff_duration %v must be greater or equal to min_startup_backoff_duration %v", config.MaxStartupBackoff, config.MinStartupBackoff)), nil
|
||||
}
|
||||
|
||||
if intervalRaw, ok := d.GetOk("interval_duration"); ok {
|
||||
|
|
@ -1975,6 +1965,13 @@ func (b *backend) pathConfigAutoTidyWrite(ctx context.Context, req *logical.Requ
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if isAutoTidyBeingEnabled {
|
||||
if err := b.updateLastAutoTidyTime(sc, time.Now()); err != nil {
|
||||
b.Logger().Warn("failed to update last auto tidy run time to now, the first auto-tidy "+
|
||||
"might run soon and not at the next delay provided", "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: getTidyConfigData(*config),
|
||||
}, nil
|
||||
|
|
@ -2114,6 +2111,24 @@ func (b *backend) tidyStatusIncCMPV2NonceDeletedCount() {
|
|||
b.tidyStatus.cmpv2NonceDeletedCount++
|
||||
}
|
||||
|
||||
// updateLastAutoTidyTime should be used to update b.lastAutoTidy as the required locks
|
||||
// are acquired and the auto tidy time is persisted to storage to work across restarts
|
||||
func (b *backend) updateLastAutoTidyTime(sc *storageContext, lastRunTime time.Time) error {
|
||||
b.tidyStatusLock.Lock()
|
||||
defer b.tidyStatusLock.Unlock()
|
||||
|
||||
b.lastAutoTidy = lastRunTime
|
||||
return sc.writeAutoTidyLastRun(lastRunTime)
|
||||
}
|
||||
|
||||
// getLastAutoTidyTime should be used to read from b.lastAutoTidy as the required locks
|
||||
// are acquired prior to reading
|
||||
func (b *backend) getLastAutoTidyTime() time.Time {
|
||||
b.tidyStatusLock.RLock()
|
||||
defer b.tidyStatusLock.RUnlock()
|
||||
return b.lastAutoTidy
|
||||
}
|
||||
|
||||
const pathTidyHelpSyn = `
|
||||
Tidy up the backend by removing expired certificates, revocation information,
|
||||
or both.
|
||||
|
|
@ -2210,6 +2225,8 @@ func getTidyConfigData(config tidyConfig) 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),
|
||||
"min_startup_backoff_duration": int(config.MinStartupBackoff.Seconds()),
|
||||
"max_startup_backoff_duration": int(config.MaxStartupBackoff.Seconds()),
|
||||
"tidy_cert_store": config.CertStore,
|
||||
"tidy_revoked_certs": config.RevokedCerts,
|
||||
"tidy_revoked_cert_issuer_associations": config.IssuerAssocs,
|
||||
|
|
|
|||
|
|
@ -236,11 +236,13 @@ func TestAutoTidy(t *testing.T) {
|
|||
|
||||
// Write the auto-tidy config.
|
||||
_, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{
|
||||
"enabled": true,
|
||||
"interval_duration": "1s",
|
||||
"tidy_cert_store": true,
|
||||
"tidy_revoked_certs": true,
|
||||
"safety_buffer": "1s",
|
||||
"enabled": true,
|
||||
"interval_duration": "1s",
|
||||
"tidy_cert_store": true,
|
||||
"tidy_revoked_certs": true,
|
||||
"safety_buffer": "1s",
|
||||
"min_startup_backoff_duration": "1s",
|
||||
"max_startup_backoff_duration": "1s",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -341,6 +343,67 @@ func TestAutoTidy(t *testing.T) {
|
|||
require.Nil(t, resp)
|
||||
}
|
||||
|
||||
// TestAutoTidyPersistsAcrossRestarts validates that on initial
|
||||
// startup of a mount we persisted the current auto tidy time so that
|
||||
// our counter that auto-tidy is based on isn't reset everytime Vault restarts
|
||||
func TestAutoTidyPersistsAcrossRestarts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newPeriod := 1 * time.Second
|
||||
|
||||
// This test requires the periodicFunc to trigger, which requires we stand
|
||||
// up a full test cluster.
|
||||
coreConfig := &vault.CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"pki": Factory,
|
||||
},
|
||||
RollbackPeriod: newPeriod,
|
||||
}
|
||||
opts := &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
NumCores: 1,
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, opts)
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Mount PKI
|
||||
err := client.Sys().Mount("pki", &api.MountInput{
|
||||
Type: "pki",
|
||||
})
|
||||
require.NoError(t, err, "failed mounting pki")
|
||||
|
||||
// Run a tidy that should set us up
|
||||
_, err = client.Logical().Write("pki/tidy", map[string]interface{}{
|
||||
"tidy_cert_store": "true",
|
||||
})
|
||||
require.NoError(t, err, "failed running tidy")
|
||||
|
||||
waitForTidyToFinish(t, client, "pki")
|
||||
|
||||
resp, err := client.Logical().Read("pki/tidy-status")
|
||||
require.NoError(t, err, "failed reading tidy status")
|
||||
require.NotNil(t, resp, "response from tidy-status was nil")
|
||||
lastAutoTidy, exists := resp.Data["last_auto_tidy_finished"]
|
||||
require.True(t, exists, "did not find last_auto_tidy_finished")
|
||||
|
||||
cluster.StopCore(t, 0)
|
||||
cluster.StartCore(t, 0, opts)
|
||||
cluster.UnsealCore(t, cluster.Cores[0])
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
|
||||
client = cluster.Cores[0].Client
|
||||
resp, err = client.Logical().Read("pki/tidy-status")
|
||||
require.NoError(t, err, "failed reading tidy status")
|
||||
require.NotNil(t, resp, "response from tidy-status was nil")
|
||||
postRestartLastAutoTidy, exists := resp.Data["last_auto_tidy_finished"]
|
||||
require.True(t, exists, "did not find last_auto_tidy_finished")
|
||||
|
||||
require.Equal(t, lastAutoTidy, postRestartLastAutoTidy, "values for last_auto_tidy_finished did not match on restart")
|
||||
}
|
||||
|
||||
func TestTidyCancellation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
@ -553,6 +616,8 @@ func TestTidyIssuerConfig(t *testing.T) {
|
|||
defaultConfigMap["pause_duration"] = time.Duration(defaultConfigMap["pause_duration"].(float64)).String()
|
||||
defaultConfigMap["revocation_queue_safety_buffer"] = int(time.Duration(defaultConfigMap["revocation_queue_safety_buffer"].(float64)) / time.Second)
|
||||
defaultConfigMap["acme_account_safety_buffer"] = int(time.Duration(defaultConfigMap["acme_account_safety_buffer"].(float64)) / time.Second)
|
||||
defaultConfigMap["min_startup_backoff_duration"] = int(time.Duration(defaultConfigMap["min_startup_backoff_duration"].(float64)) / time.Second)
|
||||
defaultConfigMap["max_startup_backoff_duration"] = int(time.Duration(defaultConfigMap["max_startup_backoff_duration"].(float64)) / time.Second)
|
||||
|
||||
require.Equal(t, defaultConfigMap, resp.Data)
|
||||
|
||||
|
|
@ -683,6 +748,8 @@ func TestCertStorageMetrics(t *testing.T) {
|
|||
"safety_buffer": "1s",
|
||||
"maintain_stored_certificate_counts": true,
|
||||
"publish_stored_certificate_count_metrics": false,
|
||||
"min_startup_backoff_duration": "1s",
|
||||
"max_startup_backoff_duration": "1s",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -1302,6 +1369,9 @@ func waitForTidyToFinish(t *testing.T, client *api.Client, mount string) *api.Se
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed reading path: %s: %w", tidyStatusPath, err)
|
||||
}
|
||||
if statusResp == nil {
|
||||
return fmt.Errorf("got nil, nil response from: %s", tidyStatusPath)
|
||||
}
|
||||
if state, ok := statusResp.Data["state"]; !ok || state == "Running" {
|
||||
return fmt.Errorf("tidy status state is still running")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ const (
|
|||
autoTidyConfigPath = "config/auto-tidy"
|
||||
clusterConfigPath = "config/cluster"
|
||||
|
||||
autoTidyLastRunPath = "config/auto-tidy-last-run"
|
||||
|
||||
maxRolesToScanOnIssuerChange = 100
|
||||
maxRolesToFindOnIssuerChange = 10
|
||||
)
|
||||
|
|
@ -713,6 +715,14 @@ func (sc *storageContext) getAutoTidyConfig() (*tidyConfig, error) {
|
|||
result.IssuerSafetyBuffer = defaultTidyConfig.IssuerSafetyBuffer
|
||||
}
|
||||
|
||||
if result.MinStartupBackoff == 0 {
|
||||
result.MinStartupBackoff = defaultTidyConfig.MinStartupBackoff
|
||||
}
|
||||
|
||||
if result.MaxStartupBackoff == 0 {
|
||||
result.MaxStartupBackoff = defaultTidyConfig.MaxStartupBackoff
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
|
@ -769,6 +779,41 @@ func (sc *storageContext) writeClusterConfig(config *issuing.ClusterConfigEntry)
|
|||
return sc.Storage.Put(sc.Context, entry)
|
||||
}
|
||||
|
||||
// tidyLastRun Track the various pieces of information around tidy on a specific cluster
|
||||
type tidyLastRun struct {
|
||||
LastRunTime time.Time
|
||||
}
|
||||
|
||||
func (sc *storageContext) getAutoTidyLastRun() (time.Time, error) {
|
||||
entry, err := sc.Storage.Get(sc.Context, autoTidyLastRunPath)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed getting auto tidy last run: %w", err)
|
||||
}
|
||||
if entry == nil {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
var result tidyLastRun
|
||||
if err = entry.DecodeJSON(&result); err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed parsing auto tidy last run: %w", err)
|
||||
}
|
||||
return result.LastRunTime, nil
|
||||
}
|
||||
|
||||
func (sc *storageContext) writeAutoTidyLastRun(lastRunTime time.Time) error {
|
||||
lastRun := tidyLastRun{LastRunTime: lastRunTime}
|
||||
entry, err := logical.StorageEntryJSON(autoTidyLastRunPath, lastRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed generating json for auto tidy last run: %w", err)
|
||||
}
|
||||
|
||||
if err := sc.Storage.Put(sc.Context, entry); err != nil {
|
||||
return fmt.Errorf("failed writing auto tidy last run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchRevocationInfo(sc pki_backend.StorageContext, serial string) (*revocation.RevocationInfo, error) {
|
||||
var revInfo *revocation.RevocationInfo
|
||||
revEntry, err := fetchCertBySerial(sc, revocation.RevokedPath, serial)
|
||||
|
|
|
|||
3
changelog/28488.txt
Normal file
3
changelog/28488.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/pki: Track the last time auto-tidy ran to address auto-tidy not running if the auto-tidy interval is longer than scheduled Vault restarts.
|
||||
```
|
||||
|
|
@ -16,7 +16,7 @@ export default class PkiTidyModel extends Model {
|
|||
label: 'Tidy ACME enabled',
|
||||
labelDisabled: 'Tidy ACME disabled',
|
||||
mapToBoolean: 'tidyAcme',
|
||||
helperTextDisabled: 'Tidying of ACME accounts, orders and authorizations is disabled',
|
||||
helperTextDisabled: 'Tidying of ACME accounts, orders and authorizations is disabled.',
|
||||
helperTextEnabled:
|
||||
'The amount of time that must pass after creation that an account with no orders is marked revoked, and the amount of time after being marked revoked or deactivated.',
|
||||
detailsLabel: 'ACME account safety buffer',
|
||||
|
|
@ -31,25 +31,45 @@ export default class PkiTidyModel extends Model {
|
|||
})
|
||||
tidyAcme;
|
||||
|
||||
// * auto tidy only fields
|
||||
@attr('boolean', {
|
||||
label: 'Automatic tidy enabled',
|
||||
defaultValue: false,
|
||||
labelDisabled: 'Automatic tidy disabled',
|
||||
helperTextDisabled: 'Automatic tidy operations will not run.',
|
||||
})
|
||||
enabled; // auto-tidy only
|
||||
enabled; // renders outside FormField loop as a toggle, auto tidy fields only render if enabled
|
||||
|
||||
@attr({
|
||||
label: 'Automatic tidy enabled',
|
||||
labelDisabled: 'Automatic tidy disabled',
|
||||
mapToBoolean: 'enabled',
|
||||
editType: 'ttl',
|
||||
helperTextEnabled:
|
||||
'Sets the interval_duration between automatic tidy operations; note that this is from the end of one operation to the start of the next.',
|
||||
helperTextDisabled: 'Automatic tidy operations will not run.',
|
||||
detailsLabel: 'Automatic tidy duration',
|
||||
hideToggle: true,
|
||||
formatTtl: true,
|
||||
})
|
||||
intervalDuration; // auto-tidy only
|
||||
intervalDuration;
|
||||
|
||||
@attr('string', {
|
||||
@attr({
|
||||
label: 'Minimum startup backoff duration',
|
||||
editType: 'ttl',
|
||||
helperTextEnabled:
|
||||
'Sets the min_startup_backoff_duration field which forces the minimum delay after Vault startup auto-tidy can run.',
|
||||
hideToggle: true,
|
||||
formatTtl: true,
|
||||
})
|
||||
minStartupBackoffDuration;
|
||||
|
||||
@attr({
|
||||
label: 'Maximum startup backoff duration',
|
||||
editType: 'ttl',
|
||||
helperTextEnabled:
|
||||
'Sets the max_startup_backoff_duration field which forces the maximum delay after Vault startup auto-tidy can run.',
|
||||
hideToggle: true,
|
||||
formatTtl: true,
|
||||
})
|
||||
maxStartupBackoffDuration;
|
||||
// * end of auto-tidy only fields
|
||||
|
||||
@attr({
|
||||
editType: 'ttl',
|
||||
helperTextEnabled:
|
||||
'Specifies a duration that issuers should be kept for, past their NotAfter validity period. Defaults to 365 days (8760 hours).',
|
||||
|
|
@ -76,7 +96,7 @@ export default class PkiTidyModel extends Model {
|
|||
})
|
||||
revocationQueueSafetyBuffer; // enterprise only
|
||||
|
||||
@attr('string', {
|
||||
@attr({
|
||||
editType: 'ttl',
|
||||
helperTextEnabled:
|
||||
'For a certificate to be expunged, the time must be after the expiration time of the certificate (according to the local clock) plus the safety buffer. Defaults to 72 hours.',
|
||||
|
|
@ -131,10 +151,16 @@ export default class PkiTidyModel extends Model {
|
|||
tidyRevokedCerts;
|
||||
|
||||
get allGroups() {
|
||||
const groups = [{ autoTidy: ['enabled', 'intervalDuration'] }, ...this.sharedFields];
|
||||
const groups = [{ autoTidy: ['enabled', ...this.autoTidyConfigFields] }, ...this.sharedFields];
|
||||
return this._expandGroups(groups);
|
||||
}
|
||||
|
||||
// fields that are specific to auto-tidy
|
||||
get autoTidyConfigFields() {
|
||||
// 'enabled' is not included here because it is responsible for hiding/showing these params and renders separately in the form
|
||||
return ['intervalDuration', 'minStartupBackoffDuration', 'maxStartupBackoffDuration'];
|
||||
}
|
||||
|
||||
// shared between auto and manual tidy operations
|
||||
get sharedFields() {
|
||||
const groups = [
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export default class PkiTidySerializer extends ApplicationSerializer {
|
|||
if (tidyType === 'manual') {
|
||||
delete data?.enabled;
|
||||
delete data?.intervalDuration;
|
||||
delete data?.minStartupBackoffDuration;
|
||||
delete data?.maxStartupBackoffDuration;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,23 +13,34 @@
|
|||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
|
||||
<form class="has-bottom-margin-s" {{on "submit" (perform this.save)}} data-test-tidy-form={{@tidyType}}>
|
||||
{{#if (and (eq @tidyType "auto") this.intervalDurationAttr)}}
|
||||
{{#let this.intervalDurationAttr as |attr|}}
|
||||
<TtlPicker
|
||||
data-test-input={{attr.name}}
|
||||
@onChange={{fn this.handleTtl attr}}
|
||||
@label={{attr.options.label}}
|
||||
@labelDisabled={{attr.options.labelDisabled}}
|
||||
@helperTextDisabled={{attr.options.helperTextDisabled}}
|
||||
@helperTextEnabled={{attr.options.helperTextEnabled}}
|
||||
@initialEnabled={{get @tidy attr.options.mapToBoolean}}
|
||||
@initialValue={{get @tidy attr.name}}
|
||||
/>
|
||||
{{#if (eq @tidyType "auto")}}
|
||||
{{#let (get @tidy.allByKey "enabled") as |enabledAttr|}}
|
||||
<div class="field">
|
||||
<Toggle @onChange={{fn (mut @tidy.enabled)}} @checked={{@tidy.enabled}} @name={{enabledAttr.name}}>
|
||||
<legend>
|
||||
<span class="ttl-picker-label is-large">
|
||||
{{if @tidy.enabled enabledAttr.options.label enabledAttr.options.labelDisabled}}
|
||||
</span>
|
||||
{{#unless @tidy.enabled}}
|
||||
<p class="sub-text">{{enabledAttr.options.helperTextDisabled}}</p>
|
||||
{{/unless}}
|
||||
</legend>
|
||||
</Toggle>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{#if @tidy.enabled}}
|
||||
<h2 class="title is-size-5 has-border-bottom-light page-header" data-test-tidy-header="Automatic tidy settings">
|
||||
Automatic tidy settings
|
||||
</h2>
|
||||
{{#each @tidy.autoTidyConfigFields as |field|}}
|
||||
<FormField @attr={{get @tidy.allByKey field}} @model={{@tidy}} />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#each @tidy.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
{{#if (or (eq @tidyType "manual") @tidy.enabled)}}
|
||||
|
||||
{{#if (or (eq @tidyType "manual") @tidy.enabled)}}
|
||||
{{#each @tidy.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
<h2 class="title is-size-5 has-border-bottom-light page-header" data-test-tidy-header={{group}}>
|
||||
{{group}}
|
||||
</h2>
|
||||
|
|
@ -52,9 +63,9 @@
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
<Hds::ButtonSet class="has-top-margin-m">
|
||||
|
|
|
|||
|
|
@ -23,11 +23,9 @@ interface Args {
|
|||
}
|
||||
|
||||
interface PkiTidyTtls {
|
||||
intervalDuration: string;
|
||||
acmeAccountSafetyBuffer: string;
|
||||
}
|
||||
interface PkiTidyBooleans {
|
||||
enabled: boolean;
|
||||
tidyAcme: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -37,10 +35,6 @@ export default class PkiTidyForm extends Component<Args> {
|
|||
@tracked errorBanner = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
|
||||
get intervalDurationAttr() {
|
||||
return this.args.tidy?.allByKey.intervalDuration;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save(event: Event) {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ module('Acceptance | pki tidy', function (hooks) {
|
|||
await click(PKI_TIDY.tidyEmptyStateConfigure);
|
||||
await click(PKI_TIDY.tidyConfigureModal.tidyModalAutoButton);
|
||||
assert.dom(PKI_TIDY_FORM.tidyFormName('auto')).exists('Auto tidy form exists');
|
||||
await click(PKI_TIDY_FORM.toggleLabel('Automatic tidy disabled'));
|
||||
await click(GENERAL.ttl.toggle('enabled'));
|
||||
assert
|
||||
.dom(PKI_TIDY_FORM.tidySectionHeader('ACME operations'))
|
||||
.exists('Auto tidy form enabled shows ACME operations field');
|
||||
|
|
@ -192,7 +192,7 @@ module('Acceptance | pki tidy', function (hooks) {
|
|||
await click(PKI_TIDY.tidyConfigureModal.tidyOptionsModal);
|
||||
assert.dom(PKI_TIDY.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
|
||||
await click(PKI_TIDY.tidyConfigureModal.tidyModalAutoButton);
|
||||
await click(PKI_TIDY_FORM.toggleLabel('Automatic tidy disabled'));
|
||||
await click(GENERAL.ttl.toggle('enabled'));
|
||||
await click(PKI_TIDY_FORM.inputByAttr('tidyCertStore'));
|
||||
await click(PKI_TIDY_FORM.inputByAttr('tidyRevokedCerts'));
|
||||
await click(PKI_TIDY_FORM.tidySave);
|
||||
|
|
|
|||
|
|
@ -1406,6 +1406,16 @@ const pki = {
|
|||
'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.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
minStartupBackoffDuration: {
|
||||
editType: 'ttl',
|
||||
helpText: 'The minimum amount of time in seconds auto-tidy will be delayed after startup.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
maxStartupBackoffDuration: {
|
||||
editType: 'ttl',
|
||||
helpText: 'The maximum amount of time in seconds auto-tidy will be delayed after startup.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
issuerSafetyBuffer: {
|
||||
editType: 'ttl',
|
||||
helpText:
|
||||
|
|
|
|||
|
|
@ -195,7 +195,6 @@ export const PKI_TIDY_FORM = {
|
|||
tidyFormName: (attr: string) => `[data-test-tidy-form="${attr}"]`,
|
||||
inputByAttr: (attr: string) => `[data-test-input="${attr}"]`,
|
||||
toggleInput: (attr: string) => `[data-test-input="${attr}"] input`,
|
||||
intervalDuration: '[data-test-ttl-value="Automatic tidy enabled"]',
|
||||
acmeAccountSafetyBuffer: '[data-test-ttl-value="Tidy ACME enabled"]',
|
||||
toggleLabel: (label: string) => `[data-test-toggle-label="${label}"]`,
|
||||
tidySectionHeader: (header: string) => `[data-test-tidy-header="${header}"]`,
|
||||
|
|
|
|||
|
|
@ -51,12 +51,14 @@ module('Integration | Component | page/pki-tidy-auto-settings', function (hooks)
|
|||
.hasText('Edit auto-tidy', 'toolbar edit link has correct text');
|
||||
|
||||
assert.dom('[data-test-row="enabled"] [data-test-label-div]').hasText('Automatic tidy enabled');
|
||||
assert.dom('[data-test-row="intervalDuration"] [data-test-label-div]').hasText('Automatic tidy duration');
|
||||
assert.dom('[data-test-value-div="Automatic tidy enabled"]').hasText('No');
|
||||
assert.dom('[data-test-value-div="Interval duration"]').hasText('2 days');
|
||||
// Universal operations
|
||||
assert.dom('[data-test-group-title="Universal operations"]').hasText('Universal operations');
|
||||
assert
|
||||
.dom('[data-test-value-div="Tidy the certificate store"]')
|
||||
.exists('Renders universal field when value exists');
|
||||
assert.dom('[data-test-value-div="Tidy the certificate store"]').hasText('No');
|
||||
assert
|
||||
.dom('[data-test-value-div="Tidy revoked certificates"]')
|
||||
.doesNotExist('Does not render universal field when value null');
|
||||
|
|
@ -65,6 +67,7 @@ module('Integration | Component | page/pki-tidy-auto-settings', function (hooks)
|
|||
assert
|
||||
.dom('[data-test-value-div="Tidy expired issuers"]')
|
||||
.exists('Renders issuer op field when value exists');
|
||||
assert.dom('[data-test-value-div="Tidy expired issuers"]').hasText('Yes');
|
||||
assert
|
||||
.dom('[data-test-value-div="Tidy legacy CA bundle"]')
|
||||
.doesNotExist('Does not render issuer op field when value null');
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { hbs } from 'ember-cli-htmlbars';
|
|||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { PKI_TIDY_FORM } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { convertToSeconds } from 'core/utils/duration-utils';
|
||||
|
||||
module('Integration | Component | pki tidy form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -24,21 +26,36 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
this.onSave = () => {};
|
||||
this.onCancel = () => {};
|
||||
this.manualTidy = this.store.createRecord('pki/tidy', { backend: 'pki-manual-tidy' });
|
||||
this.autoTidyServerDefaults = {
|
||||
enabled: false,
|
||||
interval_duration: '12h',
|
||||
safety_buffer: '3d',
|
||||
issuer_safety_buffer: '365d',
|
||||
min_startup_backoff_duration: '5m',
|
||||
max_startup_backoff_duration: '15m',
|
||||
};
|
||||
this.store.pushPayload('pki/tidy', {
|
||||
modelName: 'pki/tidy',
|
||||
id: 'pki-auto-tidy',
|
||||
// setting defaults here to simulate how this form works in the app.
|
||||
// on init, we retrieve these from the server and pre-populate form (instead of explicitly set on the model)
|
||||
...this.autoTidyServerDefaults,
|
||||
});
|
||||
this.autoTidy = this.store.peekRecord('pki/tidy', 'pki-auto-tidy');
|
||||
this.numTidyAttrs = Object.keys(this.autoTidy.allByKey).length;
|
||||
});
|
||||
|
||||
test('it hides or shows fields depending on auto-tidy toggle', async function (assert) {
|
||||
assert.expect(41);
|
||||
const sectionHeaders = [
|
||||
'Automatic tidy settings',
|
||||
'Universal operations',
|
||||
'ACME operations',
|
||||
'Issuer operations',
|
||||
'Cross-cluster operations',
|
||||
];
|
||||
const loopAssertCount = this.numTidyAttrs * 2 - 3; // loop skips 3 params
|
||||
const headerAssertCount = sectionHeaders.length * 2;
|
||||
assert.expect(loopAssertCount + headerAssertCount + 4);
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
|
|
@ -51,11 +68,13 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('intervalDuration')).isNotChecked('Automatic tidy is disabled');
|
||||
assert.dom(`[data-test-ttl-form-label="Automatic tidy disabled"]`).exists('renders disabled label text');
|
||||
assert.dom(GENERAL.toggleInput('enabled')).isNotChecked();
|
||||
assert
|
||||
.dom(GENERAL.ttl.toggle('enabled'))
|
||||
.hasText('Automatic tidy disabled Automatic tidy operations will not run.');
|
||||
|
||||
this.autoTidy.eachAttribute((attr) => {
|
||||
if (attr === 'enabled' || attr === 'intervalDuration') return;
|
||||
if (attr === 'enabled') return;
|
||||
assert
|
||||
.dom(PKI_TIDY_FORM.inputByAttr(attr))
|
||||
.doesNotExist(`does not render ${attr} when auto tidy disabled`);
|
||||
|
|
@ -66,12 +85,12 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
});
|
||||
|
||||
// ENABLE AUTO TIDY
|
||||
await click(PKI_TIDY_FORM.toggleInput('intervalDuration'));
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('intervalDuration')).isChecked('Automatic tidy is enabled');
|
||||
assert.dom(`[data-test-ttl-form-label="Automatic tidy enabled"]`).exists('renders enabled text');
|
||||
await click(GENERAL.toggleInput('enabled'));
|
||||
assert.dom(GENERAL.toggleInput('enabled')).isChecked();
|
||||
assert.dom(GENERAL.ttl.toggle('enabled')).hasText('Automatic tidy enabled');
|
||||
|
||||
this.autoTidy.eachAttribute((attr) => {
|
||||
const skipFields = ['enabled', 'tidyAcme', 'intervalDuration'];
|
||||
const skipFields = ['enabled', 'tidyAcme'];
|
||||
if (skipFields.includes(attr)) return; // combined with duration ttl or asserted elsewhere
|
||||
assert.dom(PKI_TIDY_FORM.inputByAttr(attr)).exists(`renders ${attr} when auto tidy enabled`);
|
||||
});
|
||||
|
|
@ -82,9 +101,9 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it renders all attribute fields, including enterprise', async function (assert) {
|
||||
assert.expect(29);
|
||||
assert.expect(35);
|
||||
this.autoTidy.enabled = true;
|
||||
const skipFields = ['enabled', 'tidyAcme', 'intervalDuration']; // combined with duration ttl or asserted separately
|
||||
const skipFields = ['enabled', 'tidyAcme']; // combined with duration ttl or asserted separately
|
||||
await render(
|
||||
hbs`
|
||||
<PkiTidyForm
|
||||
|
|
@ -102,6 +121,7 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
assert.dom(PKI_TIDY_FORM.inputByAttr(attr)).exists(`renders ${attr} for auto tidyType`);
|
||||
});
|
||||
|
||||
// MANUAL TIDY
|
||||
await render(
|
||||
hbs`
|
||||
<PkiTidyForm
|
||||
|
|
@ -113,11 +133,18 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('intervalDuration')).doesNotExist('hides automatic tidy toggle');
|
||||
assert.dom(GENERAL.toggleInput('enabled')).doesNotExist('hides automatic tidy toggle');
|
||||
|
||||
this.manualTidy.eachAttribute((attr) => {
|
||||
if (skipFields.includes(attr)) return;
|
||||
assert.dom(PKI_TIDY_FORM.inputByAttr(attr)).exists(`renders ${attr} for manual tidyType`);
|
||||
// auto tidy fields we shouldn't see in the manual tidy form
|
||||
if (this.manualTidy.autoTidyConfigFields.includes(attr)) {
|
||||
assert
|
||||
.dom(PKI_TIDY_FORM.inputByAttr(attr))
|
||||
.doesNotExist(`${attr} should not appear on manual tidyType`);
|
||||
} else {
|
||||
assert.dom(PKI_TIDY_FORM.inputByAttr(attr)).exists(`renders ${attr} for manual tidyType`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -176,17 +203,35 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
|
||||
test('it should change the attributes on the model', async function (assert) {
|
||||
assert.expect(12);
|
||||
// ttl picker defaults to seconds, unless unit is set by default value (set in beforeEach hook)
|
||||
// on submit, any user inputted values should be converted to seconds for the payload
|
||||
const fillInValues = {
|
||||
acmeAccountSafetyBuffer: { time: 680, unit: 'h' },
|
||||
intervalDuration: { time: 10, unit: 'h' },
|
||||
issuerSafetyBuffer: { time: 20, unit: 'd' },
|
||||
maxStartupBackoffDuration: { time: 30, unit: 'm' },
|
||||
minStartupBackoffDuration: { time: 10, unit: 'm' },
|
||||
pauseDuration: { time: 30, unit: 's' },
|
||||
revocationQueueSafetyBuffer: { time: 40, unit: 's' },
|
||||
safetyBuffer: { time: 50, unit: 'd' },
|
||||
};
|
||||
const calcValue = (param) => {
|
||||
const { time, unit } = fillInValues[param];
|
||||
return `${convertToSeconds(time, unit)}s`;
|
||||
};
|
||||
this.server.post('/pki-auto-tidy/config/auto-tidy', (schema, req) => {
|
||||
assert.propEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{
|
||||
acme_account_safety_buffer: '72h',
|
||||
acme_account_safety_buffer: '48h',
|
||||
enabled: true,
|
||||
interval_duration: '10s',
|
||||
issuer_safety_buffer: '20s',
|
||||
pause_duration: '30s',
|
||||
revocation_queue_safety_buffer: '40s',
|
||||
safety_buffer: '50s',
|
||||
min_startup_backoff_duration: calcValue('minStartupBackoffDuration'),
|
||||
max_startup_backoff_duration: calcValue('maxStartupBackoffDuration'),
|
||||
interval_duration: calcValue('intervalDuration'),
|
||||
issuer_safety_buffer: calcValue('issuerSafetyBuffer'),
|
||||
pause_duration: calcValue('pauseDuration'),
|
||||
revocation_queue_safety_buffer: calcValue('revocationQueueSafetyBuffer'),
|
||||
safety_buffer: calcValue('safetyBuffer'),
|
||||
tidy_acme: true,
|
||||
tidy_cert_metadata: true,
|
||||
tidy_cert_store: true,
|
||||
|
|
@ -213,16 +258,14 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('intervalDuration')).isNotChecked('Automatic tidy is disabled');
|
||||
assert.dom(PKI_TIDY_FORM.toggleLabel('Automatic tidy disabled')).exists('auto tidy has disabled label');
|
||||
assert.dom(GENERAL.toggleInput('enabled')).isNotChecked();
|
||||
assert.dom(GENERAL.ttl.toggle('enabled')).hasTextContaining('Automatic tidy disabled');
|
||||
assert.false(this.autoTidy.enabled, 'enabled is false on model');
|
||||
|
||||
// enable auto-tidy
|
||||
await click(PKI_TIDY_FORM.toggleInput('intervalDuration'));
|
||||
await fillIn(PKI_TIDY_FORM.intervalDuration, 10);
|
||||
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('intervalDuration')).isChecked('toggle enabled auto tidy');
|
||||
assert.dom(PKI_TIDY_FORM.toggleLabel('Automatic tidy enabled')).exists('auto tidy has enabled label');
|
||||
await click(GENERAL.toggleInput('enabled'));
|
||||
assert.dom(GENERAL.toggleInput('enabled')).isChecked();
|
||||
assert.dom(GENERAL.ttl.toggle('enabled')).hasText('Automatic tidy enabled');
|
||||
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('acmeAccountSafetyBuffer')).isNotChecked('ACME tidy is disabled');
|
||||
assert
|
||||
|
|
@ -231,28 +274,23 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
assert.false(this.autoTidy.tidyAcme, 'tidyAcme is false on model');
|
||||
|
||||
await click(PKI_TIDY_FORM.toggleInput('acmeAccountSafetyBuffer'));
|
||||
await fillIn(PKI_TIDY_FORM.acmeAccountSafetyBuffer, 3); // units are days based on defaultValue
|
||||
await fillIn(PKI_TIDY_FORM.acmeAccountSafetyBuffer, 2); // units are days based on defaultValue
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('acmeAccountSafetyBuffer')).isChecked('ACME tidy is enabled');
|
||||
assert.dom(PKI_TIDY_FORM.toggleLabel('Tidy ACME enabled')).exists('ACME label has correct enabled text');
|
||||
assert.true(this.autoTidy.tidyAcme, 'tidyAcme toggles to true');
|
||||
|
||||
const fillInValues = {
|
||||
issuerSafetyBuffer: 20,
|
||||
pauseDuration: 30,
|
||||
revocationQueueSafetyBuffer: 40,
|
||||
safetyBuffer: 50,
|
||||
};
|
||||
this.autoTidy.eachAttribute(async (attr, { type }) => {
|
||||
const skipFields = ['enabled', 'tidyAcme', 'intervalDuration', 'acmeAccountSafetyBuffer']; // combined with duration ttl or asserted separately
|
||||
const skipFields = ['enabled', 'tidyAcme', 'acmeAccountSafetyBuffer']; // combined with duration ttl or asserted separately
|
||||
if (skipFields.includes(attr)) return;
|
||||
// all params right now are either a boolean or TTL, this if/else will need to be updated if that changes
|
||||
if (type === 'boolean') {
|
||||
await click(PKI_TIDY_FORM.inputByAttr(attr));
|
||||
}
|
||||
if (type === 'string') {
|
||||
await fillIn(PKI_TIDY_FORM.toggleInput(attr), `${fillInValues[attr]}`);
|
||||
} else {
|
||||
const { time } = fillInValues[attr];
|
||||
await fillIn(PKI_TIDY_FORM.toggleInput(attr), `${time}`);
|
||||
}
|
||||
});
|
||||
|
||||
assert.dom(PKI_TIDY_FORM.toggleInput('acmeAccountSafetyBuffer')).isChecked('ACME tidy is enabled');
|
||||
assert.dom(PKI_TIDY_FORM.toggleLabel('Tidy ACME enabled')).exists('ACME label has correct enabled text');
|
||||
await click(PKI_TIDY_FORM.tidySave);
|
||||
});
|
||||
|
||||
|
|
@ -263,11 +301,11 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
|||
assert.propEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{
|
||||
...this.autoTidyServerDefaults,
|
||||
acme_account_safety_buffer: '720h',
|
||||
enabled: false,
|
||||
tidy_acme: false,
|
||||
},
|
||||
'response contains auto-tidy params'
|
||||
'response contains default auto-tidy params'
|
||||
);
|
||||
});
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
|
|
|||
2
ui/types/vault/models/pki/tidy.d.ts
vendored
2
ui/types/vault/models/pki/tidy.d.ts
vendored
|
|
@ -12,6 +12,8 @@ export default class PkiTidyModel extends Model {
|
|||
tidyAcme: boolean;
|
||||
enabled: boolean;
|
||||
intervalDuration: string;
|
||||
minStartupBackoffDuration: string;
|
||||
maxStartupBackoffDuration: string;
|
||||
issuerSafetyBuffer: string;
|
||||
pauseDuration: string;
|
||||
revocationQueueSafetyBuffer: string;
|
||||
|
|
|
|||
|
|
@ -4468,6 +4468,8 @@ $ curl \
|
|||
"interval_duration": 43200,
|
||||
"issuer_safety_buffer": 31536000,
|
||||
"maintain_stored_certificate_counts": false,
|
||||
"max_startup_backoff_duration": 1200,
|
||||
"min_startup_backoff_duration": 900,
|
||||
"pause_duration": "0s",
|
||||
"publish_stored_certificate_count_metrics": false,
|
||||
"revocation_queue_safety_buffer": 172800,
|
||||
|
|
@ -4521,6 +4523,12 @@ The below parameters are in addition to the regular parameters accepted by the
|
|||
this cluster. Instead, use audit logs and aggregate this data externally
|
||||
to Vault so as not to impact Vault performance.
|
||||
|
||||
- `min_startup_backoff_duration` `(string: "5m")` - The minimum amount of time auto-tidy
|
||||
will be delayed after startup. Defaults to 5 minutes.
|
||||
|
||||
- `max_startup_backoff_duration` `(string: "15m")` - The maximum amount of time auto-tidy
|
||||
will be delayed after startup. Defaults to 15 minutes.
|
||||
|
||||
- `publish_stored_certificate_count_metrics` `(bool: false)` - When enabled,
|
||||
publishes the value computed by `maintain_stored_certificate_counts` to
|
||||
the mount's metrics. This requires the former to be enabled.
|
||||
|
|
@ -4573,7 +4581,7 @@ The result includes the following fields:
|
|||
* `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
|
||||
* `pause_duration`: the value of this parameter when initiating the tidy operation
|
||||
* `last_auto_tidy_finished`: the time when the last auto-tidy operation finished; may be different than `time_finished` especially if the last operation was a manually executed tidy operation. Set to current time at mount time to delay the initial auto-tidy operation; not persisted.
|
||||
* `last_auto_tidy_finished`: the time when the last auto-tidy operation finished; may be different than `time_finished` especially if the last operation was a manually executed tidy operation. Set to current time at mount startup if no previous value was persisted.
|
||||
* `tidy_cert_metadata`: the value of this parameter when initiating the tidy operation
|
||||
* `cert_metadata_deleted_count`: the number of metadata entries deleted
|
||||
* `cmpv2_nonce_deleted_count`: the number of CMPv2 nonces deleted
|
||||
|
|
|
|||
Loading…
Reference in a new issue