Vault Automation e018b0c436
Implement PKIPublicCA config parsing and handling (#12363) (#13368)
* Update config.go

* added validation and parsing

* tests

* move pki external config structs and validation into separate file

* update copywrite

* update configuration

* updates

* Moved tests to pki_external_config.go, comments, refactoring

* refactor

* add tests

* linter fix

* Consolidate to table tests

* consolidate to table tests

* remove APIVersion from PKIExternalCA

* added comments for explaining each struct

* Added ParsePKIExternalCA Test

* Update tests

* Added remaining constraints

* Added destination.template field

* changes

* Added validateListenerAddr

* refactor

* more comments

* changes

* Check for duplicates across blocks

* Make RSA bits a required field

* moved template to the top level

* added comment for test explanation

* move template to the top level

* Move pki config into pkiexternalca directory

* fix linting error

* move pkiconfig back into config folder

* fix failing unit tests

* added comments

* update to preserve order of templatePKIExternalCARefs

* Added comment descriptions for each struct member

* update to include warning

* bring in warning logger from upstream into the pki config parser

* Set default umask to 077

* added comments to each field in agent config

* execute tests in parallel

* combine tests into Validate

* Use assertion error func for tests

* assert error strings

* Removed warning for now

* removed normalization on values during validation

* added tests to ensure that user values are not overridden

* remove testparse

* Update command/agent/config/config.go



* change improvement to feature in changelog

* updated to add line number in error

* Added _ent suffix to files

* Implement CA manager for ACME-based workflows (#12827)

* Implement CA manager for ACME-based workflows

* refactor tests into table tests

* update with suggestions

* format

* fix challenge cleanup

* make fmt

* update with suggestions

* add _ent + build flags

* Add a runtime component for pkiexternalca (#12838)

* Implement CA manager for ACME-based workflows

* Add a runtime component for pkiexternalca

* make fmt

* refactor tests into table tests

* update with suggestions

* format

* fix challenge cleanup

* make fmt

* update with suggestions

* update with suggestions

* add _ent + build flags

* fix linters

* delete duplicate files

* fix changelog

* rename test files

* fix linter

* try to bypass false positive linter err

* fix

* Rename file

* fix linter

* fix linter

* remove go:build enterprise commends from _ent files

* update order statuses to use kebab case + fix scanner failures

* add missing order status

* Template Integration For pki_external_ca resources (#13069)

* Implement CA manager for ACME-based workflows

* Add a runtime component for pkiexternalca

* make fmt

* refactor tests into table tests

* update with suggestions

* initial commit

* fix test failure

* changes

* remove logger check

* remove redundant config by name check

* convert to table tests

* added comments

* updates

* Fix tests

* fix nil pointer issue

* move changes to _ent files

* remove ce duplicate files

* updates

* update template.go

* added changelog.txt

* create template_pem_ent_test.go

* added comment explanation

* update ca_manager_ent.go

* update changelog

* separate ce stubs into server_ce.go and common code into server.go

* Moved helper functions to bottom of test file. Added godocs.

* Make pkiExternalCA name required in template

* remove go:build enterprise commends from _ent files

* rename to template_pem_ent

* include ent tag in server_ent.go

* remove enterprise tag comment from server_ent.go

* create pki_external_config_ce.go

* update template_pem_ent_integration_test.go

* rename integration test

---------




---------

Co-authored-by: Jaired Jawed <jaired.jawed@hashicorp.com>
Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com>
Co-authored-by: Zlaticanin <60530402+Zlaticanin@users.noreply.github.com>
Co-authored-by: Milena Zlaticanin <Milena.Zlaticanin@ibm.com>
2026-03-25 09:43:27 -04:00

692 lines
22 KiB
Go

// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"net/http"
"sync/atomic"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/backoff"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
)
// AuthMethod is the interface that auto-auth methods implement for the agent/proxy
// to use.
type AuthMethod interface {
// Authenticate returns a mount path, header, request body, and error.
// The header may be nil if no special header is needed.
Authenticate(context.Context, *api.Client) (string, http.Header, map[string]interface{}, error)
NewCreds() chan struct{}
CredSuccess()
Shutdown()
}
// AuthMethodWithClient is an extended interface that can return an API client
// for use during the authentication call.
type AuthMethodWithClient interface {
AuthMethod
AuthClient(client *api.Client) (*api.Client, error)
}
type AuthConfig struct {
Logger hclog.Logger
MountPath string
WrapTTL time.Duration
Config map[string]interface{}
}
// AuthHandler is responsible for keeping a token alive and renewed and passing
// new tokens to the sink server
//
// Each subsystem that needs a Vault token gets its own dedicated channel so that
// a slow or idle consumer cannot starve another. The enable flags gate delivery:
// a channel is only written to when the corresponding subsystem is configured,
// preventing unnecessary blocking sends to unused channels.
type AuthHandler struct {
// OutputCh delivers every new token to sink writers (e.g. file sinks).
OutputCh chan string
// TemplateTokenCh delivers tokens to the consul-template rendering server.
TemplateTokenCh chan string
// ExecTokenCh delivers tokens to the exec/env-template server.
ExecTokenCh chan string
// PKIExternalCATokenCh delivers tokens to the PKI external CA server.
// Kept separate from TemplateTokenCh so that only one consumer receives each token.
PKIExternalCATokenCh chan string
AuthInProgress *atomic.Bool
InvalidToken chan error
token string
userAgent string
metricsSignifier string
logger hclog.Logger
client *api.Client
random *rand.Rand
wrapTTL time.Duration
maxBackoff time.Duration
minBackoff time.Duration
enableReauthOnNewCredentials bool
// enable* flags mirror whether the corresponding subsystem is configured.
// Only true channels receive token sends, preventing stalls on un-configured subsystems.
enableTemplateTokenCh bool
enableExecTokenCh bool
enablePKIExternalCATokenCh bool
exitOnError bool
}
type AuthHandlerConfig struct {
Logger hclog.Logger
Client *api.Client
WrapTTL time.Duration
MaxBackoff time.Duration
MinBackoff time.Duration
Token string
// UserAgent is the HTTP UserAgent header auto-auth will use when
// communicating with Vault.
UserAgent string
// MetricsSignifier is the first argument we will give to
// metrics.IncrCounter, signifying what the name of the application is
MetricsSignifier string
EnableReauthOnNewCredentials bool
EnableTemplateTokenCh bool
EnableExecTokenCh bool
EnablePKIExternalCATokenCh bool
ExitOnError bool
}
func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler {
ah := &AuthHandler{
// This is buffered so that if we try to output after the sink server
// has been shut down, during agent/proxy shutdown, we won't block
OutputCh: make(chan string, 1),
TemplateTokenCh: make(chan string, 1),
ExecTokenCh: make(chan string, 1),
PKIExternalCATokenCh: make(chan string, 1),
InvalidToken: make(chan error, 1),
AuthInProgress: &atomic.Bool{},
token: conf.Token,
logger: conf.Logger,
client: conf.Client,
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
wrapTTL: conf.WrapTTL,
minBackoff: conf.MinBackoff,
maxBackoff: conf.MaxBackoff,
enableReauthOnNewCredentials: conf.EnableReauthOnNewCredentials,
enableTemplateTokenCh: conf.EnableTemplateTokenCh,
enableExecTokenCh: conf.EnableExecTokenCh,
enablePKIExternalCATokenCh: conf.EnablePKIExternalCATokenCh,
exitOnError: conf.ExitOnError,
userAgent: conf.UserAgent,
metricsSignifier: conf.MetricsSignifier,
}
return ah
}
func backoffSleep(ctx context.Context, backoff *autoAuthBackoff) bool {
nextSleep, err := backoff.backoff.Next()
if err != nil {
return false
}
select {
case <-time.After(nextSleep):
case <-ctx.Done():
}
return true
}
// isTransientError determines if an error should be retried using
// retryablehttp.DefaultRetryPolicy. Returns true for transient errors
// (5xx, network errors, timeouts), false for permanent errors (4xx).
func (ah *AuthHandler) isTransientError(ctx context.Context, err error) bool {
var resp *http.Response
if respErr, ok := err.(*api.ResponseError); ok {
resp = &http.Response{
StatusCode: respErr.StatusCode,
}
err = nil
}
shouldRetry, _ := retryablehttp.DefaultRetryPolicy(ctx, resp, err)
return shouldRetry
}
func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
if am == nil {
return errors.New("auth handler: nil auth method")
}
if ah.minBackoff <= 0 {
ah.minBackoff = consts.DefaultMinBackoff
}
if ah.maxBackoff <= 0 {
ah.maxBackoff = consts.DefaultMaxBackoff
}
if ah.minBackoff > ah.maxBackoff {
return errors.New("auth handler: min_backoff cannot be greater than max_backoff")
}
backoffCfg := newAutoAuthBackoff(ah.minBackoff, ah.maxBackoff, ah.exitOnError)
ah.logger.Info("starting auth handler")
// Set unauthenticated when starting up
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
defer func() {
am.Shutdown()
// Closing the token channels signals downstream consumers (template server,
// exec server, PKI server) that no further tokens will arrive and they should
// stop waiting.
close(ah.OutputCh)
close(ah.TemplateTokenCh)
close(ah.ExecTokenCh)
close(ah.PKIExternalCATokenCh)
ah.logger.Info("auth handler stopped")
// Set unauthenticated when shutting down
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
}()
credCh := am.NewCreds()
if !ah.enableReauthOnNewCredentials {
realCredCh := credCh
credCh = nil
if realCredCh != nil {
go func() {
for {
select {
case <-ctx.Done():
return
case <-realCredCh:
}
}
}()
}
}
if credCh == nil {
credCh = make(chan struct{})
}
if ah.client != nil {
headers := ah.client.Headers()
if headers == nil {
headers = make(http.Header)
}
headers.Set("User-Agent", ah.userAgent)
ah.client.SetHeaders(headers)
}
var watcher *api.LifetimeWatcher
first := true
for {
// We will unset this bool in sink.go once the token has been written to
// any sinks, or the sink server stops
ah.AuthInProgress.Store(true)
// Drain any Invalid Token errors from the channel that could have been sent before AuthInProgress
// was set to true
select {
case <-ah.InvalidToken:
ah.logger.Info("renewal already in progress, draining extra auth renewal triggers")
default:
// Do nothing, keep going
}
select {
case <-ctx.Done():
return nil
default:
}
var clientToUse *api.Client
var err error
var path string
var data map[string]interface{}
var header http.Header
var isTokenFileMethod bool
switch am.(type) {
case AuthMethodWithClient:
clientToUse, err = am.(AuthMethodWithClient).AuthClient(ah.client)
if err != nil {
ah.logger.Error("error creating client for authentication call", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
default:
clientToUse = ah.client
}
// Disable retry on the client to ensure our backoffOrQuit function is
// the only source of retry/backoff.
clientToUse.SetMaxRetries(0)
var secret *api.Secret = new(api.Secret)
if first && ah.token != "" {
ah.logger.Debug("using preloaded token")
first = false
ah.logger.Debug("lookup-self with preloaded token")
clientToUse.SetToken(ah.token)
secret, err = clientToUse.Auth().Token().LookupSelfWithContext(ctx)
if err != nil {
// Classify and handle error based on type
if ah.isTransientError(ctx, err) {
// Transient error (5xx, network, etc.) - retry the lookup
ah.logger.Warn("transient error during token lookup, will retry",
"err", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
// Reset first flag to retry lookup-self with same token
first = true
continue
}
return err
} else {
// Permanent error (4xx like 403/404) - discard token and re-authenticate
ah.logger.Error("permanent error during token lookup, will re-authenticate",
"err", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
// Clear the token so we don't retry with it
ah.token = ""
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
}
duration, _ := secret.Data["ttl"].(json.Number).Int64()
secret.Auth = &api.SecretAuth{
ClientToken: secret.Data["id"].(string),
LeaseDuration: int(duration),
Renewable: secret.Data["renewable"].(bool),
}
} else {
ah.logger.Info("authenticating")
path, header, data, err = am.Authenticate(ctx, ah.client)
if err != nil {
ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
}
if ah.wrapTTL > 0 {
wrapClient, err := clientToUse.CloneWithHeaders()
if err != nil {
ah.logger.Error("error creating client for wrapped call", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
wrapClient.SetWrappingLookupFunc(func(string, string) string {
return ah.wrapTTL.String()
})
clientToUse = wrapClient
}
for key, values := range header {
for _, value := range values {
clientToUse.AddHeader(key, value)
}
}
// This should only happen if there's no preloaded token (regular auto-auth login)
// or if a preloaded token has expired and is now switching to auto-auth.
if secret.Auth == nil {
isTokenFileMethod = path == "auth/token/lookup-self"
if isTokenFileMethod {
token, _ := data["token"].(string)
// The error is called clientErr as to not shadow the other err above it.
lookupSelfClient, clientErr := clientToUse.CloneWithHeaders()
if clientErr != nil {
ah.logger.Error("failed to clone client to perform token lookup")
return clientErr
}
lookupSelfClient.SetToken(token)
secret, err = lookupSelfClient.Auth().Token().LookupSelf()
} else {
secret, err = clientToUse.Logical().WriteWithContext(ctx, path, data)
}
// Check errors/sanity
if err != nil {
ah.logger.Error("error authenticating", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
}
var leaseDuration int
switch {
case ah.wrapTTL > 0:
if secret.WrapInfo == nil {
ah.logger.Error("authentication returned nil wrap info", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
if secret.WrapInfo.Token == "" {
ah.logger.Error("authentication returned empty wrapped client token", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
wrappedResp, err := jsonutil.EncodeJSON(secret.WrapInfo)
if err != nil {
ah.logger.Error("failed to encode wrapinfo", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
ah.logger.Info("authentication successful, sending wrapped token to sinks and pausing")
ah.OutputCh <- string(wrappedResp)
if ah.enableTemplateTokenCh {
ah.TemplateTokenCh <- string(wrappedResp)
}
if ah.enableExecTokenCh {
ah.ExecTokenCh <- string(wrappedResp)
}
if ah.enablePKIExternalCATokenCh {
ah.PKIExternalCATokenCh <- string(wrappedResp)
}
am.CredSuccess()
backoffCfg.backoff.Reset()
select {
case <-ctx.Done():
ah.logger.Info("shutdown triggered")
continue
case <-credCh:
ah.logger.Info("auth method found new credentials, re-authenticating")
continue
}
default:
// We handle the token_file method specially, as it's the only
// auth method that isn't actually authenticating, i.e. the secret
// returned does not have an Auth struct attached
isTokenFileMethod := path == "auth/token/lookup-self"
if isTokenFileMethod {
// We still check the response of the request to ensure the token is valid
// i.e. if the token is invalid, we will fail in the authentication step
if secret == nil || secret.Data == nil {
ah.logger.Error("token file validation failed, token may be invalid", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
token, ok := secret.Data["id"].(string)
if !ok || token == "" {
ah.logger.Error("token file validation returned empty client token", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
duration, _ := secret.Data["ttl"].(json.Number).Int64()
leaseDuration = int(duration)
renewable, _ := secret.Data["renewable"].(bool)
secret.Auth = &api.SecretAuth{
ClientToken: token,
LeaseDuration: int(duration),
Renewable: renewable,
}
ah.logger.Info("authentication successful, sending token to sinks")
ah.OutputCh <- token
if ah.enableTemplateTokenCh {
ah.TemplateTokenCh <- token
}
if ah.enableExecTokenCh {
ah.ExecTokenCh <- token
}
if ah.enablePKIExternalCATokenCh {
ah.PKIExternalCATokenCh <- token
}
tokenType := secret.Data["type"].(string)
if tokenType == "batch" {
ah.logger.Info("note that this token type is batch, and batch tokens cannot be renewed", "ttl", leaseDuration)
}
} else {
if secret == nil || secret.Auth == nil {
ah.logger.Error("authentication returned nil auth info", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
if secret.Auth.ClientToken == "" {
ah.logger.Error("authentication returned empty client token", "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
leaseDuration = secret.LeaseDuration
ah.logger.Info("authentication successful, sending token to sinks")
ah.OutputCh <- secret.Auth.ClientToken
if ah.enableTemplateTokenCh {
ah.TemplateTokenCh <- secret.Auth.ClientToken
}
if ah.enableExecTokenCh {
ah.ExecTokenCh <- secret.Auth.ClientToken
}
if ah.enablePKIExternalCATokenCh {
ah.PKIExternalCATokenCh <- secret.Auth.ClientToken
}
}
am.CredSuccess()
backoffCfg.backoff.Reset()
}
if watcher != nil {
watcher.Stop()
}
watcher, err = clientToUse.NewLifetimeWatcher(&api.LifetimeWatcherInput{
Secret: secret,
})
if err != nil {
ah.logger.Error("error creating lifetime watcher", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
if backoffSleep(ctx, backoffCfg) {
continue
}
return err
}
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "success"}, 1)
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 1)
// We don't want to trigger the renewal process for the root token
if isRootToken(leaseDuration, isTokenFileMethod, secret) {
ah.logger.Info("not starting token renewal process, as token is root token")
} else {
ah.logger.Info("starting renewal process")
go watcher.Renew()
}
LifetimeWatcherLoop:
for {
select {
case <-ctx.Done():
ah.logger.Info("shutdown triggered, stopping lifetime watcher")
watcher.Stop()
break LifetimeWatcherLoop
case err := <-watcher.DoneCh():
ah.logger.Info("lifetime watcher done channel triggered, re-authenticating")
if err != nil {
ah.logger.Error("error renewing token", "error", err, "backoff", backoffCfg)
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1)
// Set unauthenticated when authentication fails
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 0)
// Add some exponential backoff so that if auth is successful
// but the watcher errors, we won't go into an immediate
// aggressive retry loop.
// This might be quite a small sleep, since if we have a successful
// auth, we reset the backoff. Still, some backoff is important, and
// ensuring we follow the normal flow is important:
// auth -> try to renew
if !backoffSleep(ctx, backoffCfg) {
// We're at max retries. Return an error.
return fmt.Errorf("exceeded max retries failing to renew auth token")
}
}
// If the lease duration is 0, wait a second before re-authenticating
// so that we don't go into a loop, as the LifetimeWatcher will immediately
// return for tokens like this.
if leaseDuration == 0 {
time.Sleep(1 * time.Second)
}
break LifetimeWatcherLoop
case <-watcher.RenewCh():
metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "success"}, 1)
// Set authenticated when authentication succeeds
metrics.SetGauge([]string{ah.metricsSignifier, "authenticated"}, 1)
ah.logger.Info("renewed auth token")
case <-credCh:
ah.logger.Info("auth method found new credentials, re-authenticating")
break LifetimeWatcherLoop
case <-ah.InvalidToken:
ah.logger.Info("invalid token found, re-authenticating")
break LifetimeWatcherLoop
}
}
}
}
// isRootToken checks if the secret in the argument is the root token
// This is determinable without leaseDuration and isTokenFileMethod,
// but those make it easier to rule out other tokens cheaply.
func isRootToken(leaseDuration int, isTokenFileMethod bool, secret *api.Secret) bool {
// This check is cheaper than the others, so we do this first.
if leaseDuration == 0 && isTokenFileMethod && !secret.Renewable {
if secret != nil {
policies, err := secret.TokenPolicies()
if err == nil {
if len(policies) == 1 && policies[0] == "root" {
return true
}
}
}
}
return false
}
// autoAuthBackoff tracks exponential backoff state.
type autoAuthBackoff struct {
backoff *backoff.Backoff
}
func newAutoAuthBackoff(min, max time.Duration, exitErr bool) *autoAuthBackoff {
if max <= 0 {
max = consts.DefaultMaxBackoff
}
if min <= 0 {
min = consts.DefaultMinBackoff
}
retries := math.MaxInt
if exitErr {
retries = 0
}
b := backoff.NewBackoff(retries, min, max)
return &autoAuthBackoff{
backoff: b,
}
}
func (b autoAuthBackoff) String() string {
return b.backoff.Current().Truncate(10 * time.Millisecond).String()
}