// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package leafcert
import (
"time"
"github.com/hashicorp/consul/agent/structs"
)
// calculateSoftExpiry encapsulates our logic for when to renew a cert based on
// it's age. It returns a pair of times min, max which makes it easier to test
// the logic without non-deterministic jitter to account for. The caller should
// choose a time randomly in between these.
//
// We want to balance a few factors here:
// - renew too early and it increases the aggregate CSR rate in the cluster
// - renew too late and it risks disruption to the service if a transient
// error prevents the renewal
// - we want a broad amount of jitter so if there is an outage, we don't end
// up with all services in sync and causing a thundering herd every
// renewal period. Broader is better for smoothing requests but pushes
// both earlier and later tradeoffs above.
//
// Somewhat arbitrarily the current strategy looks like this:
//
// 0 60% 90%
// Issued [------------------------------|===============|!!!!!] Expires
// 72h TTL: 0 ~43h ~65h
// 1h TTL: 0 36m 54m
//
// Where |===| is the soft renewal period where we jitter for the first attempt
// and |!!!| is the danger zone where we just try immediately.
//
// In the happy path (no outages) the average renewal occurs half way through
// the soft renewal region or at 75% of the cert lifetime which is ~54 hours for
// a 72 hour cert, or 45 mins for a 1 hour cert.
//
// If we are already in the softRenewal period, we randomly pick a time between
// now and the start of the danger zone.
//
// We pass in now to make testing easier.
func calculateSoftExpiry ( now time . Time , cert * structs . IssuedCert ) ( min time . Time , max time . Time ) {
certLifetime := cert . ValidBefore . Sub ( cert . ValidAfter )
if certLifetime < 10 * time . Minute {
// Shouldn't happen as we limit to 1 hour shortest elsewhere but just be
// defensive against strange times or bugs.
return now , now
}
// Find the 60% mark in diagram above
softRenewTime := cert . ValidAfter . Add ( time . Duration ( float64 ( certLifetime ) * 0.6 ) )
hardRenewTime := cert . ValidAfter . Add ( time . Duration ( float64 ( certLifetime ) * 0.9 ) )
if now . After ( hardRenewTime ) {
// In the hard renew period, or already expired. Renew now!
return now , now
}
if now . After ( softRenewTime ) {
// Already in the soft renew period, make now the lower bound for jitter
softRenewTime = now
}
return softRenewTime , hardRenewTime
}