// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package leafcert import ( "sync" "time" "golang.org/x/time/rate" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib/ttlcache" ) // certData tracks all of the metadata about a leaf cert. type certData struct { // lock locks access to all fields lock sync.Mutex // index is the last raft index associated with an update of the 'value' field index uint64 // value is the last updated cert contents or nil if not populated initially value *structs.IssuedCert // state is metadata related to cert generation state fetchState // fetchedAt was the time when 'value' was last updated fetchedAt time.Time // refreshing indicates if there is an active request attempting to refresh // the current leaf cert contents. refreshing bool // lastFetchErr is the last error encountered when attempting to populate // the 'value' field. lastFetchErr error // expiry contains information about the expiration of this // cert. This is a pointer as its shared as a value in the // ExpiryHeap as well. expiry *ttlcache.Entry // refreshRateLimiter limits the rate at which the cert can be regenerated refreshRateLimiter *rate.Limiter } func (c *certData) MarkRefreshing(v bool) { c.lock.Lock() defer c.lock.Unlock() c.refreshing = v } func (c *certData) GetValueAndState() (*structs.IssuedCert, fetchState) { c.lock.Lock() defer c.lock.Unlock() return c.value, c.state } func (c *certData) GetError() error { c.lock.Lock() defer c.lock.Unlock() return c.lastFetchErr } // NOTE: this function only has one goroutine in it per key at all times func (c *certData) Update( newCert *structs.IssuedCert, newState fetchState, err error, ) { c.lock.Lock() defer c.lock.Unlock() // Importantly, always reset the Error. Having both Error and a Value that // are non-nil is allowed in the cache entry but it indicates that the Error // is _newer_ than the last good value. So if the err is nil then we need to // reset to replace any _older_ errors and avoid them bubbling up. If the // error is non-nil then we need to set it anyway and used to do it in the // code below. See https://github.com/hashicorp/consul/issues/4480. c.lastFetchErr = err c.state = newState if newCert != nil { c.index = newCert.ModifyIndex c.value = newCert c.fetchedAt = time.Now() } if c.index < 1 { // Less than one is invalid unless there was an error and in this case // there wasn't since a value was returned. If a badly behaved RPC // returns 0 when it has no data, we might get into a busy loop here. We // set this to minimum of 1 which is safe because no valid user data can // ever be written at raft index 1 due to the bootstrap process for // raft. This insure that any subsequent background refresh request will // always block, but allows the initial request to return immediately // even if there is no data. c.index = 1 } } // fetchState is some additional metadata we store with each cert in the cache // to track things like expiry and coordinate paces root rotations. It's // important this doesn't contain any pointer types since we rely on the struct // being copied to avoid modifying the actual state in the cache entry during // Fetch. Pointers themselves are OK, but if we point to another struct that we // call a method or modify in some way that would directly mutate the cache and // cause problems. We'd need to deep-clone in that case in Fetch below. // time.Time technically contains a pointer to the Location but we ignore that // since all times we get from our wall clock should point to the same Location // anyway. type fetchState struct { // authorityKeyId is the ID of the CA key (whether root or intermediate) that signed // the current cert. This is just to save parsing the whole cert everytime // we have to check if the root changed. authorityKeyID string // forceExpireAfter is used to coordinate renewing certs after a CA rotation // in a staggered way so that we don't overwhelm the servers. forceExpireAfter time.Time // activeRootRotationStart is set when the root has changed and we need to get // a new cert but haven't got one yet. forceExpireAfter will be set to the // next scheduled time we should try our CSR, but this is needed to calculate // the retry windows if we are rate limited when we try. See comment on // const caChangeJitterWindow above for more. activeRootRotationStart time.Time // consecutiveRateLimitErrs stores how many rate limit errors we've hit. We // use this to choose a new window for the next retry. See comment on // const caChangeJitterWindow above for more. consecutiveRateLimitErrs int }