Secrets, PKI & CertificatesXIII · Dynamic Credentials and Workload IdentityDynamicCredentials
Authenticating machines: AppRole, Kubernetes, JWT and client certificates
What you'll learn
- State for each auth method what the workload presents and what verifies it
- Configure an AppRole login and read the resulting token response correctly
- Explain why the Kubernetes method calls TokenReview instead of checking a signature
- Choose an auth method from the properties of the platform rather than from familiarity
Prerequisites
Practice
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Before a workload can be issued a dynamic credential, it has to be recognised. That is a separate problem from authorisation, and it is where the real architecture lives. Every machine authentication method answers the same two questions in a different way: what does the workload hand over, and which authority is consulted to decide whether the claim is true? Hold those two questions steady and the methods stop looking like a menu and start looking like a spectrum.
What is presented, and what verifies it
The spectrum runs from methods where the workload presents something it stores on disk, to methods where it presents something the surrounding platform produced about it moments earlier. Both ends issue the same kind of token at the end; they differ entirely in what had to be true beforehand.
| Method | The workload presents | The verifying authority |
|---|---|---|
| AppRole | A role identifier and a secret identifier it holds | The secret manager, comparing against its own stored records |
| Kubernetes | Its projected ServiceAccount token | The Kubernetes API server, via the TokenReview API |
| JWT and OIDC | A signed token issued by a trusted issuer | The issuer’s published signing keys, plus bound claims |
| TLS certificate | A client certificate during the handshake | A configured trust anchor, plus certificate constraints |
The built-in set in a stock binary is approle, cert, jwt,
kerberos, kubernetes, ldap, oidc, radius and userpass.
There is no cloud IAM method in the binary. The AWS, Azure, GCP,
AliCloud and OCI methods live outside the core distribution as
separately released plugins, so a runbook that opens with enabling one
of them on a stock install does not work. Three of the built-ins,
kerberos, ldap and radius, are deprecated and slated to leave the
main distribution, which is worth knowing before you build on them.
flowchart LR
A["Workload"] --> B{"What does it\npresent?"}
B -- "stored secret" --> C["AppRole\nverified against stored records"]
B -- "platform token" --> D["Kubernetes\nverified by TokenReview"]
B -- "signed JWT" --> E["JWT or OIDC\nverified against issuer keys"]
B -- "client certificate" --> F["Cert method\nverified against a trust anchor"]
C --> G["Token issued\nwith bound policies and TTL"]
D --> G
E --> G
F --> G
Whichever branch is taken, the output is identical: a token carrying a set of policies and its own lease. The choice of branch decides whether a stolen disk image is enough to impersonate the workload, and that is the entire point of the comparison.
AppRole: a role identifier and a secret identifier
AppRole splits the credential in two on purpose. The RoleID identifies which role is logging in and is a unique identifier rather than a password. The SecretID is the part intended to remain confidential. The split exists so the two halves can travel by different routes and be governed by different rules.
bao auth enable approle
bao write auth/approle/role/app-role \
token_policies=app-read \
token_ttl=20m \
token_max_ttl=1h
ROLE_ID=$(bao read -field=role_id auth/approle/role/app-role/role-id)
SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/app-role/secret-id)
bao write -format=json auth/approle/login \
role_id="$ROLE_ID" \
secret_id="$SECRET_ID"
"auth": {
"client_token": "s.<REDACTED>",
"accessor": "n5GSr33ga2aoj00X03Z3y4Qh",
"policies": ["app-read", "default"],
"token_policies": ["app-read", "default"],
"metadata": {"role_name": "app-role"},
"orphan": true,
"lease_duration": 1200,
"renewable": true
}
Read that response the way an operator should. The client_token is
the credential; everything else is metadata about it. The accessor is
the safe handle: it identifies the token for revocation and audit
without being usable to authenticate, which is why it belongs in logs
and the token itself never does. The lease_duration of 1200 seconds
is the token_ttl of twenty minutes honoured exactly, and
token_max_ttl of one hour is the ceiling that renewal cannot pass.
orphan: true matters more than it looks: this token has no parent, so
revoking anything above it will not cancel it, and it must be revoked
directly or by accessor.
AppRole gives you real constraints on the secret half, and they are the
difference between a design and a liability. bind_secret_id decides
whether a SecretID is required at all. secret_id_ttl expires it.
secret_id_num_uses limits how many logins it can perform, and setting
it to one turns the SecretID into a single-use bootstrap token.
secret_id_bound_cidrs restricts which source addresses may present
it. A SecretID with no TTL, unlimited uses and no address binding is
functionally a permanent password with extra steps.
Kubernetes: the cluster vouches for the pod
In a cluster, a pod already holds a credential it did not have to be given: its projected ServiceAccount token, mounted by the kubelet and rotated automatically. The Kubernetes auth method uses that token as the thing the workload presents, which means nothing has to be distributed to the pod at all.
The critical detail is what verifies it. The secret manager does not validate the token’s signature and accept it. It calls the Kubernetes TokenReview API and asks the cluster whether the token is still valid. That is a live question rather than a cryptographic one, and it is what makes the method honour a token that has been invalidated since it was issued.
bao auth enable kubernetes
bao write auth/kubernetes/config \
kubernetes_host=https://kubernetes.default.svc \
kubernetes_ca_cert="$K8S_CA_CERT" \
token_reviewer_jwt="$REVIEWER_JWT"
bao write auth/kubernetes/role/reporting \
bound_service_account_names=reporting \
bound_service_account_namespaces=analytics \
audience=openbao \
token_policies=app-read \
token_ttl=20m
The token_reviewer_jwt is a ServiceAccount token belonging to the
secret manager itself, whose ServiceAccount must hold the
system:auth-delegator cluster role in order to call TokenReview. That
is a real dependency: it is a credential the secret manager holds
against the cluster, and it has to be maintained. The bound fields are
the authorisation surface. A role bound to a ServiceAccount name but
not a namespace will accept a pod running that name in any namespace,
which is a mistake that reads as correct in review.
Two operational facts belong together here. Revocation correctness depends on the API server running with service account lookup enabled, which has been the default for many releases, and without it a deleted token in Kubernetes is not properly rejected. And modern clusters issue short-lived, pod-bound tokens rather than the long-lived Secret-backed tokens that older documentation assumes, which changes what the reviewer token itself should be. Where that becomes awkward, the documented alternative is to abandon the Kubernetes method entirely and treat the cluster as an OIDC issuer through the JWT method, so that every client uses short-lived tokens and no reviewer credential is needed.
JWT and OIDC: the issuer’s signature is the proof
The JWT method removes the verifying authority from the request path. Instead of asking a live service whether a token is good, the secret manager fetches the issuer’s published signing keys, verifies the signature offline, and then checks that the claims inside the token match conditions configured in advance. Nothing is stored on the workload, and nothing is called at login time except key retrieval.
This is the same shape as cloud workload identity federation, and it is worth recognising the pattern now because the next two lessons build on it. What makes it safe is not the signature alone. A valid signature only proves the issuer produced the token; it says nothing about which workload the token was issued to, or which relying party it was meant for. Safety comes from binding the subject claim and the audience claim, so that a token issued for a different workload, or intended for a different recipient, is rejected even though its signature verifies perfectly.
The TLS certificate method is the same idea expressed in the transport rather than in a token. The workload presents a client certificate during the handshake, the secret manager validates the chain against a configured trust anchor, and identity is taken from the certificate’s own fields. It has the useful property that possession of the private key is proved by the handshake rather than by handing a bearer value over, and the awkward property that it needs a certificate lifecycle, which is the subject of the entire first half of this course.
Production discipline
- Bind every claim you can name. A role that binds a ServiceAccount name but no namespace, or an audience but no subject, accepts a wider population than its author intended and looks correct in a code review.
- Constrain the SecretID or do not use AppRole. Give it a TTL, a use count and, where the network allows, a source address binding. An unconstrained SecretID is a static password.
- Log the accessor and never the token. The accessor supports revocation and audit correlation without being usable, which makes it the right identifier to carry through your logging pipeline.
- Know which methods your binary actually contains. Cloud IAM methods are external plugins, and three built-ins are on their way out of the core distribution. Verify before designing around one.
- Prefer the method that requires nothing on the workload. If the platform already issues an attested identity, use it. Distributing a secret to obtain secrets is the problem the next lesson takes apart.
Cross-course references
- Kubernetes for Production Sysadmins - Part LVII (Authentication) covers how the API server decides who a request came from, which is the authority the TokenReview call in this lesson consults.
- Linux for Production Sysadmins - Part XXVII (Auth) covers the host-level authentication stack, and the same separation of presenting a credential from verifying it applies there.
- Git, CI/CD & GitOps for Infrastructure Engineers - Part XLII (CISecrets) covers how a pipeline holds credentials, which is where a poorly delivered SecretID usually ends up.
Quiz
Knowledge check · 4 questions
Q1. Why does the Kubernetes auth method call the TokenReview API rather than verifying the ServiceAccount token's signature directly?
Q2. A JWT whose signature verifies against the issuer's published keys is sufficient proof that it was issued to the workload now presenting it.
Q3. Name three constraints that can be applied to an AppRole SecretID, and say what each one limits.
Q4. Decide whether the design is safe and what you would change.
A team runs eleven services in the analytics namespace of one cluster. Each service authenticates with AppRole. The RoleID is set in a ConfigMap and the SecretID is baked into the container image at build time, with no TTL and no use limit. The team points out that images are private and the registry requires authentication.
Passing score: 75%. Answers are checked in this browser.