The most surprising thing about Vault’s JWT authentication method is how it transforms a stateless, verifiable token into a stateful, authorized identity within Vault, despite the common misconception that JWTs are inherently stateless.
Let’s see it in action. Imagine you have a service that needs to authenticate to Vault. Instead of managing static tokens or complex PKI, it presents a JSON Web Token (JWT) issued by an external Identity Provider (IdP) like Auth0, Okta, or even a custom OIDC provider.
{
"iss": "https://your-oidc-provider.com/",
"sub": "user-12345",
"aud": "vault-aud-value",
"exp": 1678886400,
"iat": 1678882800,
"name": "Alice Smith",
"email": "alice@example.com",
"groups": ["developers", "ops"]
}
Vault, configured to trust this IdP, receives this JWT. It doesn’t just blindly accept it. Vault first verifies the JWT’s signature against the public keys it knows for the issuer. Then, it checks claims like iss (issuer) and aud (audience) to ensure the token is intended for Vault and comes from a trusted source.
Once validated, Vault uses the information within the JWT – specifically, claims like sub (subject) and groups – to look up or create an identity within its own internal identity store. This is where the transformation happens. The JWT, a self-contained assertion, is used to establish a persistent, role-based identity within Vault’s secure boundary.
Here’s how you set this up:
First, enable the JWT auth method:
vault auth enable jwt
Next, configure the OIDC provider details. You need the oidc_discovery_url which Vault will poll to get the issuer’s signing keys and other metadata. You also specify the bound_issuer to ensure tokens are only accepted from this specific IdP. The token_audiences claim is crucial for ensuring the JWT was issued specifically for Vault.
vault write auth/jwt/config \
oidc_discovery_url="https://your-oidc-provider.com/.well-known/openid-configuration" \
bound_issuer="https://your-oidc-provider.com/" \
token_audiences="vault-aud-value"
Now, define roles that map JWT claims to Vault policies. A role specifies which JWT claims must be present and what values they must have, and then associates Vault policies with matching tokens.
For example, let’s create a role for developers who have a specific sub claim and are members of the developers group.
vault write auth/jwt/role/developer-role \
user_claim="sub" \
groups_claim="groups" \
token_policies="developer-policy" \
bound_claims_type="glob" \
bound_claims="groups=developers"
In this role:
user_claim="sub"tells Vault to use thesubclaim from the JWT as the unique identifier for the authenticated user in Vault.groups_claim="groups"indicates that thegroupsclaim in the JWT contains an array of group memberships.token_policies="developer-policy"assigns the Vault policy nameddeveloper-policyto any identity authenticated through this role.bound_claims="groups=developers"is a powerful filter: it ensures that only JWTs containingdeveloperswithin theirgroupsclaim will be allowed to authenticate against thisdeveloper-role.
When a service presents a JWT that satisfies these conditions, Vault will issue a Vault token with the developer-policy attached, and the identity in Vault will be user-12345 (from the sub claim).
The real magic is that Vault doesn’t store the JWT itself long-term for authorization. Instead, it uses the validated JWT to create or identify a Vault identity and then associates policies with that identity. Subsequent requests from that identity (using the Vault token) are then authorized against these policies, effectively bridging the stateless JWT world to Vault’s stateful authorization model.
A common pitfall is misconfiguring token_audiences. If the JWT’s aud claim doesn’t exactly match the token_audiences configured in Vault, authentication will fail even if the signature and issuer are correct. This claim is critical for preventing token replay attacks where a token intended for one service is mistakenly accepted by another.
Once you have successfully set up JWT authentication and defined roles, the next logical step is to explore how to integrate this with dynamic secrets, allowing your applications to obtain specific, short-lived credentials based on their authenticated JWT identity.