Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Unreleased

- Documented the `bound_audiences` requirement for JWT auth, which is the most common reason a
first login fails and was previously absent from the README. Vault requires a `jwt` role to bind
the audience the token carries, but does not catch the omission when the role is created: as long
as the role has some other bound constraint it is accepted, and the failure only appears at login
as `audience claim found in JWT but no audiences bound to the role`. The JWT section now gives
that error verbatim, the wrong-audience variant, and a `vault write .../role/...` example that
binds the audience. The GitHub Actions section additionally spells out that
`core.getIDToken('vault')` mints `aud: vault`, so the role must bind `vault` — following that
snippet without it was a guaranteed failed login. Also recorded that Vault does not require an
`exp` claim, so a `jwtProvider` that omits one produces a credential that never expires. Every
quoted error string was taken from a live Vault and is identical on 1.21 and 2.0. No code change.

# 2.2.0 Release notes (2026-09-01)

- Documented three things about the HTTP transport that were true but unwritten, and scoped the
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,34 @@ Exactly one of three mutually exclusive `config` keys supplies the JWT:
be minted per login — GitHub Actions' `core.getIDToken()`, a cloud metadata endpoint, a
SPIFFE/SPIRE workload API.

##### The Vault role needs `bound_audiences` when your JWT carries an `aud` claim

This is server-side configuration rather than a client option, but it is the most common reason a
first login fails. Vault requires a `jwt` role to bind the audience your token carries, and it does
**not** catch the omission when the role is created — as long as the role has some other bound
constraint (`bound_subject`, `bound_claims`, ...) it is accepted, and the problem only surfaces at
login:

```
VaultHttpError: 400 - {"errors":["audience claim found in JWT but no audiences bound to the role"]}
```

A wrong (rather than missing) audience fails with `error validating token: invalid audience (aud)
claim: audience claim does not match any expected audience`. So bind the audience the token
actually has:

```shell
vault write auth/jwt/role/my-app \
role_type=jwt user_claim=sub bound_audiences=my-audience token_policies=my-policy
```

A role with *no* bound constraint at all is rejected when you create it (`must have at least one
bound constraint when creating/updating a role`), so that case is self-correcting. An `aud` array
is fine — Vault matches `bound_audiences` against any entry.

Vault also does not require an `exp` claim: a token minted without one is accepted and never
expires. If you write your own `jwtProvider`, give the tokens it mints a short `exp`.

#### Authenticating from GitHub Actions

```yaml
Expand All @@ -212,6 +240,18 @@ const vaultClient = VaultClient.boot('ci', {
});
```

`core.getIDToken('vault')` mints a token whose `aud` is `vault`, so the role has to bind that
audience or the login fails with `audience claim found in JWT but no audiences bound to the role`:

```shell
vault write auth/gha/role/ci \
role_type=jwt user_claim=sub bound_audiences=vault \
bound_claims='{"repository":"my-org/my-repo"}' token_policies=ci
```

Pass the same string to `core.getIDToken()` and to `bound_audiences`. Calling `getIDToken()` with
no argument uses GitHub's default audience instead, which then will not match.

`role` is optional here too — omit it to use the mount's `default_role`. `mount` and
`api.namespace` behave exactly as they do for the other four backends.

Expand Down