Vault Error: 'cannot create resource tokenreviews' and 'service account name not authorized' on Kubernetes Auth
Fix Vault Kubernetes auth login failures: grant system:auth-delegator to the token reviewer, fix kubernetes_host and CA cert, bound service account names, audiences, and issuer validation.
- #vault
- #secrets
- #security-hardening
- #troubleshooting
- #errors
Stuck on this HashiCorp Vault error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Exact Error Message
$ vault write auth/kubernetes/login role=app jwt="$SA_TOKEN"
Error writing data to auth/kubernetes/login: Error making API request.
URL: PUT https://vault.example.com:8200/v1/auth/kubernetes/login
Code: 500. Errors:
* error validating token: failed to review token: tokenreviews.authentication.k8s.io
is forbidden: User "system:serviceaccount:vault:vault" cannot create resource
"tokenreviews" in API group "authentication.k8s.io" at the cluster scope
A different misconfiguration produces a 400 instead:
Code: 400. Errors:
* service account name not authorized
What It Means
Vault’s Kubernetes auth method does not verify a service account token on its own. It hands the JWT to the Kubernetes API server’s TokenReview endpoint (authentication.k8s.io/v1, POST /apis/authentication.k8s.io/v1/tokenreviews) and asks the cluster whether the token is valid and which service account it belongs to. The first error is Kubernetes RBAC refusing that TokenReview call: the identity Vault used — either the token_reviewer_jwt configured on the mount or, when that field is empty, the login JWT itself — has no permission to create tokenreviews. Vault reports the rejection as a 500 because the review request failed rather than returned “not authenticated.”
The second error is a different stage entirely. The TokenReview succeeded, Kubernetes returned a valid identity such as system:serviceaccount:payments:api, and Vault then compared that identity against the role’s bound_service_account_names and bound_service_account_namespaces. service account name not authorized means the name did not match; the namespace equivalent reports namespace not authorized. So a 500 is an infrastructure or RBAC problem, while a 400 is a policy-binding problem on the Vault role.
Common Causes
- The reviewer service account has no
ClusterRoleBindingto the built-insystem:auth-delegatorClusterRole. token_reviewer_jwtis unset (or expired) so Vault attempts the review using the caller’s own token, which lacks TokenReview rights.kubernetes_hostpoints at an address the Vault process cannot reach, orkubernetes_ca_certdoes not match the API server’s serving certificate.- The role’s
bound_service_account_names/bound_service_account_namespacesdo not include the workload actually logging in. - The projected service account token was issued for a different audience than the role’s
audiencefield expects. - The token’s
issclaim does not match what Vault expects on a managed cluster (EKS, GKE, AKS) that uses an external OIDC issuer URL.
Diagnostic Commands
Read the mount configuration and confirm the host, CA, and issuer settings:
vault read auth/kubernetes/config
Inspect the role that is failing, paying attention to the bound fields and audience:
vault read auth/kubernetes/role/app
From inside a pod using the workload’s service account, read the projected token and try the login by hand:
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
vault write auth/kubernetes/login role=app jwt="$SA_TOKEN"
Decode the token’s claims to see the issuer, audience, namespace, and expiry:
echo "$SA_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Check whether the reviewer service account is actually allowed to create TokenReviews:
kubectl auth can-i create tokenreviews \
--as=system:serviceaccount:vault:vault
Confirm Vault can reach the API server from wherever it runs:
kubectl exec -n vault vault-0 -- \
wget -qO- --no-check-certificate https://kubernetes.default.svc/version
Watch the Vault audit or server log while a login is attempted:
kubectl logs -n vault vault-0 --tail=50 | grep -i kubernetes
Step-by-Step Resolution
- Create the reviewer service account and bind it to
system:auth-delegator, the ClusterRole that grants exactly the TokenReview and SubjectAccessReview permissions this flow needs:
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: vault
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault
namespace: vault
- Configure the auth mount. When Vault runs inside the cluster it can use the pod’s own projected token and CA bundle:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
Recent Vault versions can omit token_reviewer_jwt when running in-cluster and will re-read the pod’s projected token from disk on each review, which avoids the expiry problem described in step 3. If you set the field explicitly, you are pinning a static string.
- For an external Vault, mint a long-lived reviewer token deliberately and point at the API server’s externally reachable address. Create a bound secret rather than relying on a short projected token that will expire:
apiVersion: v1
kind: Secret
metadata:
name: vault-reviewer-token
namespace: vault
annotations:
kubernetes.io/service-account.name: vault
type: kubernetes.io/service-account-token
REVIEWER_JWT=$(kubectl get secret vault-reviewer-token -n vault \
-o go-template='{{.data.token | base64decode}}')
K8S_CA=$(kubectl get secret vault-reviewer-token -n vault \
-o go-template='{{index .data "ca.crt" | base64decode}}')
vault write auth/kubernetes/config \
kubernetes_host="https://api.cluster.example.com" \
kubernetes_ca_cert="$K8S_CA" \
token_reviewer_jwt="$REVIEWER_JWT"
- Fix the role bindings so the 400 goes away. The names and namespaces are lists, and
*is accepted as a wildcard — use it sparingly, because a wildcard on both fields lets any pod in the cluster assume the role:
vault write auth/kubernetes/role/app \
bound_service_account_names=api,worker \
bound_service_account_namespaces=payments \
token_policies=payments-read \
token_ttl=1h
- Reconcile audiences. Modern clusters issue projected tokens with a specific
audclaim, and Vault will reject a token whose audience does not match the role. Either set the role’saudienceto what the workload actually requests, or project a token with the audience the role expects:
vault write auth/kubernetes/role/app \
bound_service_account_names=api \
bound_service_account_namespaces=payments \
audience="vault" \
token_policies=payments-read
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: vault-token
audience: vault
expirationSeconds: 600
- Handle issuer mismatches on managed clusters. If logins fail with a validation error mentioning the issuer, check the token’s
issclaim against the cluster’s OIDC issuer and set the mount’sissuerfield to match. Leavedisable_iss_validationalone unless you understand the tradeoff — it stops Vault checking who issued the token, which weakens the trust chain:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
vault write auth/kubernetes/config \
kubernetes_host="https://api.cluster.example.com" \
kubernetes_ca_cert="$K8S_CA" \
token_reviewer_jwt="$REVIEWER_JWT" \
issuer="https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE"
If login now succeeds but subsequent secret reads fail, the problem has moved from authentication to authorization — see Vault error: “permission denied on path” for reading the resulting token’s policies, and Vault error: “missing client token” if the token never made it into the request at all.
Prevention
- Keep the
system:auth-delegatorClusterRoleBinding in the same manifest or chart that deploys Vault so the two never drift apart. - Prefer running Vault in-cluster with no static
token_reviewer_jwt, letting it re-read the projected token, rather than pinning a JWT that silently expires. - Create one role per workload with tight
bound_service_account_namesandbound_service_account_namespaces; avoid wildcards on both fields simultaneously. - Set an explicit
audienceon roles and request the matching audience in the pod’s projected token volume. - Store
kubernetes_ca_certfrom the cluster’s own CA bundle rather than trusting system roots, and re-run the config step whenever the API server certificate is rotated. - Add a CI smoke test that performs a real
vault write auth/kubernetes/loginfrom a throwaway pod after every cluster or Vault upgrade.
Related Errors
permission deniedon login — the role exists but the policy attached to the resulting token lacks capabilities.x509: certificate signed by unknown authority—kubernetes_ca_certor the Vault listener CA is wrong, not an RBAC failure.Vault is sealed— the server is up but cannot serve any auth request until unsealed.namespace not authorized— the sibling of the name error, caused bybound_service_account_namespaces.
Frequently Asked Questions
Why is the error a 500 rather than a 401? Because the TokenReview call itself failed. Vault never got an answer about the token’s validity, so it reports an internal error rather than an authentication rejection. A genuine bad token returns a 400 with a validation message instead.
Do I still need token_reviewer_jwt? Only when Vault runs outside the cluster or under a service account that differs from the reviewer. In-cluster Vault on recent versions can leave it empty and use its own projected token, which is preferable because it rotates automatically. See Vault error: “certificate signed by unknown authority” if the connection to the API server fails after you change this.
My login worked yesterday and fails today with no config change. The most common cause is an expired token_reviewer_jwt. Projected tokens are short-lived by design, so a JWT copied out of a pod once will stop working. Use a bound kubernetes.io/service-account-token Secret or let Vault read the token from disk.
Should I set disable_iss_validation? Treat it as a diagnostic, not a fix. Turning it off tells you the failure is issuer-related, but leaving it off permanently removes a check on token provenance. Set the issuer field to the cluster’s real OIDC issuer instead. More Vault troubleshooting is collected in the Vault guides.
Fixed it? Get 500 HashiCorp Vault & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.