Skip to content

The token does not renew itself

Of the four provider modules in this estate, this is the only one whose value goes stale. It is worth being precise about why, because the difference is structural rather than incidental.

A credential, or a way to get one

The question that decides whether a value is safe to hold indefinitely:

Does the object know how to obtain a fresh credential, or is it already a credential?

Provider What you hold Renews?
AWS a provider chain behind aws.NewCredentialsCache yes, on expiry
Azure a TokenCredential the pipeline re-calls yes, before expiry
GCP a cached token provider yes, once stale
Vault a client carrying an already-minted token no

vaultapi.NewClient reads VAULT_TOKEN a single time and stores it on the client. Nothing re-reads the environment afterwards, and nothing renews the lease. The Vault SDK models a client as configured with a token rather than configured with a way to get a token.

What that means in practice

A command that starts, reads configuration, and exits will never notice. Its token outlives it.

A long-lived process reloading configuration will fail once the TTL passes — and it fails permanently, not transiently. Every subsequent call is refused, and nothing recovers, because the client cannot learn that its credential lapsed. That is the worst shape a failure can take: it looks like a transient outage and behaves like a broken deployment.

Why this module does not fix it for you

It would need to detect that the token had expired, and then re-resolve.

The first half is the problem. A refusal from Vault might mean your token expired, or your token is fine and this path is not permitted, or you are being rate limited. Those want opposite responses, and re-resolving on the wrong one either loops forever or aims a growing number of concurrent authentications at a service already refusing them.

An estate primitive exists for exactly this shape — clientlifecycle.Invalidatable — and it is deliberately not used here, for that reason. Its own documentation makes the ability to distinguish those three cases a prerequisite for using it, not an improvement to it. This module cannot distinguish them, so it does not pretend to.

What to do instead

Renew the token, which is a lifecycle you own. Keep a vaultapi.LifetimeWatcher running against the client, and rebuild when renewal finally fails. The how-to has the shape.

Or accept it deliberately. A token with a long TTL, in a process that is redeployed more often than the token expires, is a legitimate choice — not a defect. What is not legitimate is being surprised by it.

The same is true one level up

config-vault's zero-conf rung has exactly this property, and says so on Default itself. So does config-consul, for the same structural reason — both HashiCorp SDKs model the client the same way, and Consul additionally reads its token file at construction, though a token file is precisely the mechanism you would rotate.

The ladder did not cause any of this: injecting a client always behaved this way. What a zero-conf rung changes is visibility — when you build the client, the token is in your hands; when something builds it for you, nobody confronts it. Which is why the modules where it is true say so loudly.