package pki import ( "context" "crypto/x509" "fmt" "sync/atomic" "time" "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) func pathTidy(b *backend) *framework.Path { return &framework.Path{ Pattern: "tidy", Fields: map[string]*framework.FieldSchema{ "tidy_cert_store": &framework.FieldSchema{ Type: framework.TypeBool, Description: `Set to true to enable tidying up the certificate store`, Default: false, }, "tidy_revocation_list": &framework.FieldSchema{ Type: framework.TypeBool, Description: `Set to true to enable tidying up the revocation list`, Default: false, }, "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `The amount of extra time that must have passed beyond certificate expiration before it is removed from the backend storage and/or revocation list. Defaults to 72 hours.`, Default: 259200, //72h, but TypeDurationSecond currently requires defaults to be int }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathTidyWrite, }, HelpSynopsis: pathTidyHelpSyn, HelpDescription: pathTidyHelpDesc, } } 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) tidyRevocationList := d.Get("tidy_revocation_list").(bool) if safetyBuffer < 1 { return logical.ErrorResponse("safety_buffer must be greater than zero"), nil } bufferDuration := time.Duration(safetyBuffer) * time.Second 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, } go func() { defer atomic.StoreUint32(b.tidyCASGuard, 0) // Don't cancel when the original client request goes away ctx = context.Background() logger := b.Logger().Named("tidy") doTidy := func() error { if tidyCertStore { serials, err := req.Storage.List(ctx, "certs/") if err != nil { return errwrap.Wrapf("error fetching list of certs: {{err}}", err) } for _, serial := range serials { certEntry, err := req.Storage.Get(ctx, "certs/"+serial) if err != nil { return errwrap.Wrapf(fmt.Sprintf("error fetching certificate %q: {{err}}", 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 errwrap.Wrapf(fmt.Sprintf("error deleting nil entry with serial %s: {{err}}", serial), err) } } 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 errwrap.Wrapf(fmt.Sprintf("error deleting entry with nil value with serial %s: {{err}}", serial), err) } } cert, err := x509.ParseCertificate(certEntry.Value) if err != nil { return errwrap.Wrapf(fmt.Sprintf("unable to parse stored certificate with serial %q: {{err}}", serial), err) } if time.Now().After(cert.NotAfter.Add(bufferDuration)) { if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil { return errwrap.Wrapf(fmt.Sprintf("error deleting serial %q from storage: {{err}}", serial), err) } } } } if tidyRevocationList { b.revokeStorageLock.Lock() defer b.revokeStorageLock.Unlock() tidiedRevoked := false revokedSerials, err := req.Storage.List(ctx, "revoked/") if err != nil { return errwrap.Wrapf("error fetching list of revoked certs: {{err}}", err) } var revInfo revocationInfo for _, serial := range revokedSerials { revokedEntry, err := req.Storage.Get(ctx, "revoked/"+serial) if err != nil { return errwrap.Wrapf(fmt.Sprintf("unable to fetch revoked cert with serial %q: {{err}}", 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 errwrap.Wrapf(fmt.Sprintf("error deleting nil revoked entry with serial %s: {{err}}", serial), err) } } 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 errwrap.Wrapf(fmt.Sprintf("error deleting revoked entry with nil value with serial %s: {{err}}", serial), err) } } err = revokedEntry.DecodeJSON(&revInfo) if err != nil { return errwrap.Wrapf(fmt.Sprintf("error decoding revocation entry for serial %q: {{err}}", serial), err) } revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) if err != nil { return errwrap.Wrapf(fmt.Sprintf("unable to parse stored revoked certificate with serial %q: {{err}}", serial), err) } if time.Now().After(revokedCert.NotAfter.Add(bufferDuration)) { if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil { return errwrap.Wrapf(fmt.Sprintf("error deleting serial %q from revoked list: {{err}}", serial), err) } tidiedRevoked = true } } if tidiedRevoked { if err := buildCRL(ctx, b, req); err != nil { return err } } } return nil } if err := doTidy(); err != nil { logger.Error("error running tidy", "error", err) return } }() resp := &logical.Response{} resp.AddWarning("Tidy operation successfully started. Any information from the operation will be printed to Vault's server logs.") return resp, nil } 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. `