KubernetesLX · ServiceAccountsServiceAccounts
TokenRequest API — programmatic token issuance
What you'll learn
- Call the TokenRequest API directly to issue a projected token for any SA
- Set the audience, duration, and bound-object fields to scope the token
- Use TokenRequest from CI/CD, debugging tools, and third-party integrations
- Diagnose common TokenRequest failures (denied by admission, RBAC for tokenrequests)
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The TokenRequest API is the programmatic interface for
issuing projected ServiceAccount tokens. A caller
(typically the kubelet, kubectl, or a third-party
integration) POSTs a TokenRequest spec; the API server
returns a signed JWT. This lesson covers the API
directly, the request and response shape, the audience
and duration controls, and the production use cases.
The API endpoint
POST /apis/authentication.k8s.io/v1/tokenrequests
Content-Type: application/json
Authorization: Bearer <caller's credentials>
The caller authenticates with their own credentials
(client cert, OIDC token, or another SA’s token). The
caller must have permission to create tokens for the
target SA — typically the serviceaccounts/token
subresource with verb create.
// Request body
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"spec": {
"audiences": ["https://kubernetes.default.svc"],
"expirationSeconds": 3600,
"boundObjectRef": {
"kind": "Pod",
"apiVersion": "v1",
"name": "api-7f8b9c"
}
}
}
The response:
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"status": {
"token": "eyJhbGciOiJSUzI1NiIs...",
"expirationTimestamp": "2026-08-16T15:00:00Z"
}
}
sequenceDiagram
participant C as Caller
participant AS as API server
C->>AS: POST /tokenrequests (TokenRequest spec)
AS->>AS: Verify caller can create tokens for SA
AS->>AS: Sign JWT with service-account-signing-key
AS->>C: TokenRequest status (token + expiry)
Using kubectl create token
The most common caller is kubectl create token, which
is a thin wrapper around the API:
# Basic token for a SA
kubectl create token ci-runner -n ci
# With audience
kubectl create token vault-issuer -n prod \
--audience=vault://prod.example.com
# With duration (max 1h6m for bound tokens)
kubectl create token ci-runner -n ci \
--duration=15m
# Bound to a specific Pod
kubectl create token myapp -n prod \
--bound-object-kind=Pod \
--bound-object-name=api-7f8b9c
The output is the token to stdout:
TOKEN=$(kubectl create token ci-runner -n ci --duration=15m)
echo "$TOKEN"
# eyJhbGciOiJSUzI1NiIs...
Calling the API directly
For programmatic integration (e.g., a CI/CD system that is not kubectl), the API is called with curl:
# Build the TokenRequest spec
cat > token-request.json <<EOF
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"spec": {
"audiences": ["https://kubernetes.default.svc"],
"expirationSeconds": 900
}
}
EOF
# POST to the API (authenticated with the caller's credentials)
TOKEN=$(curl -sk -X POST \
-H "Authorization: Bearer $CALLER_TOKEN" \
-H "Content-Type: application/json" \
--data @token-request.json \
https://api.example.com:6443/apis/authentication.k8s.io/v1/tokenrequests \
| jq -r .status.token)
The caller authenticates with their own token (or client cert). The target SA is identified in the URL path:
POST /apis/authentication.k8s.io/v1/namespaces/{namespace}/serviceaccounts/{name}/token
Wait — the path-based form is the older form. In Kubernetes 1.34, the body specifies the SA; the URL is the cluster-wide endpoint:
POST /apis/authentication.k8s.io/v1/tokenrequests
with the SA reference in the body.
Bound objects
A bound token is rejected unless the request is made from the bound object:
{
"spec": {
"audiences": ["https://kubernetes.default.svc"],
"expirationSeconds": 3600,
"boundObjectRef": {
"kind": "Pod",
"apiVersion": "v1",
"name": "api-7f8b9c",
"uid": "..."
}
}
}
The bound object is verified at authentication: the request must come from a Pod with the matching UID. A token leaked to a different Pod is rejected.
Production use cases
Three use cases:
- CI/CD pipelines. The pipeline authenticates with OIDC or a client cert, requests a token for the CI SA, uses the token to apply manifests. The token is short-lived; no long-lived Secret.
TOKEN=$(kubectl create token ci-runner -n ci --duration=15m)
kubectl --token="$TOKEN" apply -f manifest.yaml
- Debugging. An operator requests a token for a SA and impersonates it for testing:
TOKEN=$(kubectl create token ci-runner -n ci)
kubectl --token="$TOKEN" auth whoami
# system:serviceaccount:ci:ci-runner
- Third-party integrations. ArgoCD, Vault, Flux, and other integrations request tokens for their SAs to authenticate to the cluster. The tokens are short-lived and audience-scoped.
Common failure modes
serviceaccounts/tokenpermission denied. The caller does not havecreateon the SA’s token subresource. The fix is to grant the verb.- Audience mismatch. The caller requested
aud=vaultbut the workload is trying to use the token against the API server. The fix is to set the audience correctly. - Bound object mismatch. The caller bound the
token to Pod
api-7f8b9c(UID...) but the Pod has been restarted and has a new UID. The fix is to use a fresh Pod UID or to skip the bound form. - Maximum duration exceeded. The caller
requested
--duration=24hbut the API server’s--service-account-max-token-expirationis 1h6m. The fix is to request a shorter duration.
Production failure modes
- Tokens are issued without
aud. A token with the default audience works against every consumer that trusts the signing key. The fix is to setaudexplicitly. - Tokens are issued without
boundObjectRef. A token that is not bound can be exfiltrated. The fix is to bind to the calling Pod. - Tokens are cached by the caller. A CI system that caches a token across runs has the same credential-management problem as a long-lived Secret. The fix is to issue a new token per run.
Cross-course references
- The Linux course covers JWT and JWKS — the primitives that TokenRequest uses.
- The Observability course covers the audit log entries for TokenRequest operations.
Quiz
Knowledge check · 4 questions
Q1. What RBAC permission does a caller need to use the TokenRequest API to issue a token for a SA?
Q2. A token issued by TokenRequest with a `boundObjectRef` to a specific Pod is rejected if it is exfiltrated to a different Pod.
Q3. Your CI pipeline runs `kubectl create token ci-runner -n ci --duration=15m` and receives `403 Forbidden: User "ci-system" cannot create resource "serviceaccounts/token" in API group "authentication.k8s.io" in the namespace "ci"`. Why, and how do you fix it?
The CI system authenticates with a client cert for `ci-system`. The CI SA `ci-runner` exists in the `ci` namespace. The CI system does not have permission to issue tokens for the CI SA. The CI pipeline has been working for months; this is the first failure.
Q4. Describe the shape of a TokenRequest request and response.
Passing score: 75%. Answers are checked in this browser.
Production discipline
The TokenRequest API is the right primitive for programmatic token issuance. A defensible programme uses the minimum duration, the right audience, and bound objects where possible. Tokens are issued per run, not cached. The audit log records every TokenRequest operation with the caller’s identity and the target SA. A cluster where tokens are issued without audience or bound objects has a credential-management failure; a cluster where every token is scoped and short-lived has a credential programme that is auditable.