mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 02:02:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			421 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) HashiCorp, Inc.
 | |
| // SPDX-License-Identifier: MPL-2.0
 | |
| 
 | |
| package api
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"math/rand"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/cenkalti/backoff/v3"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	ErrLifetimeWatcherMissingInput  = errors.New("missing input")
 | |
| 	ErrLifetimeWatcherMissingSecret = errors.New("missing secret")
 | |
| 	ErrLifetimeWatcherNotRenewable  = errors.New("secret is not renewable")
 | |
| 	ErrLifetimeWatcherNoSecretData  = errors.New("returned empty secret data")
 | |
| 
 | |
| 	// Deprecated; kept for compatibility
 | |
| 	ErrRenewerMissingInput  = errors.New("missing input to renewer")
 | |
| 	ErrRenewerMissingSecret = errors.New("missing secret to renew")
 | |
| 	ErrRenewerNotRenewable  = errors.New("secret is not renewable")
 | |
| 	ErrRenewerNoSecretData  = errors.New("returned empty secret data")
 | |
| 
 | |
| 	// DefaultLifetimeWatcherRenewBuffer is the default size of the buffer for renew
 | |
| 	// messages on the channel.
 | |
| 	DefaultLifetimeWatcherRenewBuffer = 5
 | |
| 	// Deprecated: kept for backwards compatibility
 | |
| 	DefaultRenewerRenewBuffer = 5
 | |
| )
 | |
| 
 | |
| type RenewBehavior uint
 | |
| 
 | |
| const (
 | |
| 	// RenewBehaviorIgnoreErrors means we will attempt to keep renewing until
 | |
| 	// we hit the lifetime threshold. It also ignores errors stemming from
 | |
| 	// passing a non-renewable lease in. In practice, this means you simply
 | |
| 	// reauthenticate/refetch credentials when the watcher exits. This is the
 | |
| 	// default.
 | |
| 	RenewBehaviorIgnoreErrors RenewBehavior = iota
 | |
| 
 | |
| 	// RenewBehaviorRenewDisabled turns off renewal attempts entirely. This
 | |
| 	// allows you to simply watch lifetime and have the watcher return at a
 | |
| 	// reasonable threshold without actually making Vault calls.
 | |
| 	RenewBehaviorRenewDisabled
 | |
| 
 | |
| 	// RenewBehaviorErrorOnErrors is the "legacy" behavior which always exits
 | |
| 	// on some kind of error
 | |
| 	RenewBehaviorErrorOnErrors
 | |
| )
 | |
| 
 | |
| // LifetimeWatcher is a process for watching lifetime of a secret.
 | |
| //
 | |
| //	watcher, err := client.NewLifetimeWatcher(&LifetimeWatcherInput{
 | |
| //		Secret: mySecret,
 | |
| //	})
 | |
| //	go watcher.Start()
 | |
| //	defer watcher.Stop()
 | |
| //
 | |
| //	for {
 | |
| //		select {
 | |
| //		case err := <-watcher.DoneCh():
 | |
| //			if err != nil {
 | |
| //				log.Fatal(err)
 | |
| //			}
 | |
| //
 | |
| //			// Renewal is now over
 | |
| //		case renewal := <-watcher.RenewCh():
 | |
| //			log.Printf("Successfully renewed: %#v", renewal)
 | |
| //		}
 | |
| //	}
 | |
| //
 | |
| // `DoneCh` will return if renewal fails, or if the remaining lease duration is
 | |
| // under a built-in threshold and either renewing is not extending it or
 | |
| // renewing is disabled.  In both cases, the caller should attempt a re-read of
 | |
| // the secret. Clients should check the return value of the channel to see if
 | |
| // renewal was successful.
 | |
| type LifetimeWatcher struct {
 | |
| 	l sync.Mutex
 | |
| 
 | |
| 	client        *Client
 | |
| 	secret        *Secret
 | |
| 	grace         time.Duration
 | |
| 	random        *rand.Rand
 | |
| 	increment     int
 | |
| 	doneCh        chan error
 | |
| 	renewCh       chan *RenewOutput
 | |
| 	renewBehavior RenewBehavior
 | |
| 
 | |
| 	stopped bool
 | |
| 	stopCh  chan struct{}
 | |
| 
 | |
| 	errLifetimeWatcherNotRenewable error
 | |
| 	errLifetimeWatcherNoSecretData error
 | |
| }
 | |
| 
 | |
| // LifetimeWatcherInput is used as input to the renew function.
 | |
| type LifetimeWatcherInput struct {
 | |
| 	// Secret is the secret to renew
 | |
| 	Secret *Secret
 | |
| 
 | |
| 	// DEPRECATED: this does not do anything.
 | |
| 	Grace time.Duration
 | |
| 
 | |
| 	// Rand is the randomizer to use for underlying randomization. If not
 | |
| 	// provided, one will be generated and seeded automatically. If provided, it
 | |
| 	// is assumed to have already been seeded.
 | |
| 	Rand *rand.Rand
 | |
| 
 | |
| 	// RenewBuffer is the size of the buffered channel where renew messages are
 | |
| 	// dispatched.
 | |
| 	RenewBuffer int
 | |
| 
 | |
| 	// The new TTL, in seconds, that should be set on the lease. The TTL set
 | |
| 	// here may or may not be honored by the vault server, based on Vault
 | |
| 	// configuration or any associated max TTL values. If specified, the
 | |
| 	// minimum of this value and the remaining lease duration will be used
 | |
| 	// for grace period calculations.
 | |
| 	Increment int
 | |
| 
 | |
| 	// RenewBehavior controls what happens when a renewal errors or the
 | |
| 	// passed-in secret is not renewable.
 | |
| 	RenewBehavior RenewBehavior
 | |
| }
 | |
| 
 | |
| // RenewOutput is the metadata returned to the client (if it's listening) to
 | |
| // renew messages.
 | |
| type RenewOutput struct {
 | |
| 	// RenewedAt is the timestamp when the renewal took place (UTC).
 | |
| 	RenewedAt time.Time
 | |
| 
 | |
| 	// Secret is the underlying renewal data. It's the same struct as all data
 | |
| 	// that is returned from Vault, but since this is renewal data, it will not
 | |
| 	// usually include the secret itself.
 | |
| 	Secret *Secret
 | |
| }
 | |
| 
 | |
| // NewLifetimeWatcher creates a new renewer from the given input.
 | |
| func (c *Client) NewLifetimeWatcher(i *LifetimeWatcherInput) (*LifetimeWatcher, error) {
 | |
| 	if i == nil {
 | |
| 		return nil, ErrLifetimeWatcherMissingInput
 | |
| 	}
 | |
| 
 | |
| 	secret := i.Secret
 | |
| 	if secret == nil {
 | |
| 		return nil, ErrLifetimeWatcherMissingSecret
 | |
| 	}
 | |
| 
 | |
| 	random := i.Rand
 | |
| 	if random == nil {
 | |
| 		// NOTE:
 | |
| 		// Rather than a cryptographically secure random number generator (RNG),
 | |
| 		// the default behavior uses the math/rand package. The random number is
 | |
| 		// used to introduce a slight jitter when calculating the grace period
 | |
| 		// for a monitored secret monitoring. This is intended to stagger renewal
 | |
| 		// requests to the Vault server, but in a semi-predictable way, so there
 | |
| 		// is no need to use a cryptographically secure RNG.
 | |
| 		random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
 | |
| 	}
 | |
| 
 | |
| 	renewBuffer := i.RenewBuffer
 | |
| 	if renewBuffer == 0 {
 | |
| 		renewBuffer = DefaultLifetimeWatcherRenewBuffer
 | |
| 	}
 | |
| 
 | |
| 	return &LifetimeWatcher{
 | |
| 		client:        c,
 | |
| 		secret:        secret,
 | |
| 		increment:     i.Increment,
 | |
| 		random:        random,
 | |
| 		doneCh:        make(chan error, 1),
 | |
| 		renewCh:       make(chan *RenewOutput, renewBuffer),
 | |
| 		renewBehavior: i.RenewBehavior,
 | |
| 
 | |
| 		stopped: false,
 | |
| 		stopCh:  make(chan struct{}),
 | |
| 
 | |
| 		errLifetimeWatcherNotRenewable: ErrLifetimeWatcherNotRenewable,
 | |
| 		errLifetimeWatcherNoSecretData: ErrLifetimeWatcherNoSecretData,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // Deprecated: exists only for backwards compatibility. Calls
 | |
| // NewLifetimeWatcher, and sets compatibility flags.
 | |
| func (c *Client) NewRenewer(i *LifetimeWatcherInput) (*LifetimeWatcher, error) {
 | |
| 	if i == nil {
 | |
| 		return nil, ErrRenewerMissingInput
 | |
| 	}
 | |
| 
 | |
| 	secret := i.Secret
 | |
| 	if secret == nil {
 | |
| 		return nil, ErrRenewerMissingSecret
 | |
| 	}
 | |
| 
 | |
| 	renewer, err := c.NewLifetimeWatcher(i)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	renewer.renewBehavior = RenewBehaviorErrorOnErrors
 | |
| 	renewer.errLifetimeWatcherNotRenewable = ErrRenewerNotRenewable
 | |
| 	renewer.errLifetimeWatcherNoSecretData = ErrRenewerNoSecretData
 | |
| 	return renewer, err
 | |
| }
 | |
| 
 | |
| // DoneCh returns the channel where the renewer will publish when renewal stops.
 | |
| // If there is an error, this will be an error.
 | |
| func (r *LifetimeWatcher) DoneCh() <-chan error {
 | |
| 	return r.doneCh
 | |
| }
 | |
| 
 | |
| // RenewCh is a channel that receives a message when a successful renewal takes
 | |
| // place and includes metadata about the renewal.
 | |
| func (r *LifetimeWatcher) RenewCh() <-chan *RenewOutput {
 | |
| 	return r.renewCh
 | |
| }
 | |
| 
 | |
| // Stop stops the renewer.
 | |
| func (r *LifetimeWatcher) Stop() {
 | |
| 	r.l.Lock()
 | |
| 	defer r.l.Unlock()
 | |
| 
 | |
| 	if !r.stopped {
 | |
| 		close(r.stopCh)
 | |
| 		r.stopped = true
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Start starts a background process for watching the lifetime of this secret.
 | |
| // If renewal is enabled, when the secret has auth data, this attempts to renew
 | |
| // the auth (token); When the secret has a lease, this attempts to renew the
 | |
| // lease.
 | |
| func (r *LifetimeWatcher) Start() {
 | |
| 	r.doneCh <- r.doRenew()
 | |
| }
 | |
| 
 | |
| // Renew is for compatibility with the legacy api.Renewer. Calling Renew
 | |
| // simply chains to Start.
 | |
| func (r *LifetimeWatcher) Renew() {
 | |
| 	r.Start()
 | |
| }
 | |
| 
 | |
| type renewFunc func(string, int) (*Secret, error)
 | |
| 
 | |
| // doRenew is a helper for renewing authentication.
 | |
| func (r *LifetimeWatcher) doRenew() error {
 | |
| 	defaultInitialRetryInterval := 10 * time.Second
 | |
| 	switch {
 | |
| 	case r.secret.Auth != nil:
 | |
| 		return r.doRenewWithOptions(true, !r.secret.Auth.Renewable,
 | |
| 			r.secret.Auth.LeaseDuration, r.secret.Auth.ClientToken,
 | |
| 			r.client.Auth().Token().RenewTokenAsSelf, defaultInitialRetryInterval)
 | |
| 	default:
 | |
| 		return r.doRenewWithOptions(false, !r.secret.Renewable,
 | |
| 			r.secret.LeaseDuration, r.secret.LeaseID,
 | |
| 			r.client.Sys().Renew, defaultInitialRetryInterval)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (r *LifetimeWatcher) doRenewWithOptions(tokenMode bool, nonRenewable bool, initLeaseDuration int, credString string,
 | |
| 	renew renewFunc, initialRetryInterval time.Duration,
 | |
| ) error {
 | |
| 	if credString == "" ||
 | |
| 		(nonRenewable && r.renewBehavior == RenewBehaviorErrorOnErrors) {
 | |
| 		return r.errLifetimeWatcherNotRenewable
 | |
| 	}
 | |
| 
 | |
| 	initialTime := time.Now()
 | |
| 	priorDuration := time.Duration(initLeaseDuration) * time.Second
 | |
| 	r.calculateGrace(priorDuration, time.Duration(r.increment)*time.Second)
 | |
| 	var errorBackoff backoff.BackOff
 | |
| 
 | |
| 	for {
 | |
| 		// Check if we are stopped.
 | |
| 		select {
 | |
| 		case <-r.stopCh:
 | |
| 			return nil
 | |
| 		default:
 | |
| 		}
 | |
| 
 | |
| 		var remainingLeaseDuration time.Duration
 | |
| 		fallbackLeaseDuration := initialTime.Add(priorDuration).Sub(time.Now())
 | |
| 		var renewal *Secret
 | |
| 		var err error
 | |
| 
 | |
| 		switch {
 | |
| 		case nonRenewable || r.renewBehavior == RenewBehaviorRenewDisabled:
 | |
| 			// Can't or won't renew, just keep the same expiration so we exit
 | |
| 			// when it's reauthentication time
 | |
| 			remainingLeaseDuration = fallbackLeaseDuration
 | |
| 
 | |
| 		default:
 | |
| 			// Renew the token
 | |
| 			renewal, err = renew(credString, r.increment)
 | |
| 			if err != nil || renewal == nil || (tokenMode && renewal.Auth == nil) {
 | |
| 				if r.renewBehavior == RenewBehaviorErrorOnErrors {
 | |
| 					if err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 					if renewal == nil || (tokenMode && renewal.Auth == nil) {
 | |
| 						return r.errLifetimeWatcherNoSecretData
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				// Calculate remaining duration until initial token lease expires
 | |
| 				remainingLeaseDuration = initialTime.Add(time.Duration(initLeaseDuration) * time.Second).Sub(time.Now())
 | |
| 				if errorBackoff == nil {
 | |
| 					errorBackoff = &backoff.ExponentialBackOff{
 | |
| 						MaxElapsedTime:      remainingLeaseDuration,
 | |
| 						RandomizationFactor: backoff.DefaultRandomizationFactor,
 | |
| 						InitialInterval:     initialRetryInterval,
 | |
| 						MaxInterval:         5 * time.Minute,
 | |
| 						Multiplier:          2,
 | |
| 						Clock:               backoff.SystemClock,
 | |
| 					}
 | |
| 					errorBackoff.Reset()
 | |
| 				}
 | |
| 				break
 | |
| 			}
 | |
| 			errorBackoff = nil
 | |
| 
 | |
| 			// Push a message that a renewal took place.
 | |
| 			select {
 | |
| 			case r.renewCh <- &RenewOutput{time.Now().UTC(), renewal}:
 | |
| 			default:
 | |
| 			}
 | |
| 
 | |
| 			// Possibly error if we are not renewable
 | |
| 			if ((tokenMode && !renewal.Auth.Renewable) || (!tokenMode && !renewal.Renewable)) &&
 | |
| 				r.renewBehavior == RenewBehaviorErrorOnErrors {
 | |
| 				return r.errLifetimeWatcherNotRenewable
 | |
| 			}
 | |
| 
 | |
| 			// Reset initial time
 | |
| 			initialTime = time.Now()
 | |
| 
 | |
| 			// Grab the lease duration
 | |
| 			initLeaseDuration = renewal.LeaseDuration
 | |
| 			if tokenMode {
 | |
| 				initLeaseDuration = renewal.Auth.LeaseDuration
 | |
| 			}
 | |
| 
 | |
| 			remainingLeaseDuration = time.Duration(initLeaseDuration) * time.Second
 | |
| 		}
 | |
| 
 | |
| 		var sleepDuration time.Duration
 | |
| 
 | |
| 		if errorBackoff == nil {
 | |
| 			sleepDuration = r.calculateSleepDuration(remainingLeaseDuration, priorDuration)
 | |
| 		} else if errorBackoff.NextBackOff() == backoff.Stop {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		// remainingLeaseDuration becomes the priorDuration for the next loop
 | |
| 		priorDuration = remainingLeaseDuration
 | |
| 
 | |
| 		// If we are within grace, return now; or, if the amount of time we
 | |
| 		// would sleep would land us in the grace period. This helps with short
 | |
| 		// tokens; for example, you don't want a current lease duration of 4
 | |
| 		// seconds, a grace period of 3 seconds, and end up sleeping for more
 | |
| 		// than three of those seconds and having a very small budget of time
 | |
| 		// to renew.
 | |
| 		if remainingLeaseDuration <= r.grace || remainingLeaseDuration-sleepDuration <= r.grace {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		timer := time.NewTimer(sleepDuration)
 | |
| 		select {
 | |
| 		case <-r.stopCh:
 | |
| 			timer.Stop()
 | |
| 			return nil
 | |
| 		case <-timer.C:
 | |
| 			continue
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // calculateSleepDuration calculates the amount of time the LifeTimeWatcher should sleep
 | |
| // before re-entering its loop.
 | |
| func (r *LifetimeWatcher) calculateSleepDuration(remainingLeaseDuration, priorDuration time.Duration) time.Duration {
 | |
| 	// We keep evaluating a new grace period so long as the lease is
 | |
| 	// extending. Once it stops extending, we've hit the max and need to
 | |
| 	// rely on the grace duration.
 | |
| 	if remainingLeaseDuration > priorDuration {
 | |
| 		r.calculateGrace(remainingLeaseDuration, time.Duration(r.increment)*time.Second)
 | |
| 	}
 | |
| 
 | |
| 	// The sleep duration is set to 2/3 of the current lease duration plus
 | |
| 	// 1/3 of the current grace period, which adds jitter.
 | |
| 	return time.Duration(float64(remainingLeaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
 | |
| }
 | |
| 
 | |
| // calculateGrace calculates the grace period based on the minimum of the
 | |
| // remaining lease duration and the token increment value; it also adds some
 | |
| // jitter to not have clients be in sync.
 | |
| func (r *LifetimeWatcher) calculateGrace(leaseDuration, increment time.Duration) {
 | |
| 	minDuration := leaseDuration
 | |
| 	if minDuration > increment && increment > 0 {
 | |
| 		minDuration = increment
 | |
| 	}
 | |
| 
 | |
| 	if minDuration <= 0 {
 | |
| 		r.grace = 0
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	leaseNanos := float64(minDuration.Nanoseconds())
 | |
| 	jitterMax := 0.1 * leaseNanos
 | |
| 
 | |
| 	// For a given lease duration, we want to allow 80-90% of that to elapse,
 | |
| 	// so the remaining amount is the grace period
 | |
| 	r.grace = time.Duration(jitterMax) + time.Duration(uint64(r.random.Int63())%uint64(jitterMax))
 | |
| }
 | |
| 
 | |
| type (
 | |
| 	Renewer      = LifetimeWatcher
 | |
| 	RenewerInput = LifetimeWatcherInput
 | |
| )
 | 
