Humans via SSO, workloads via service accounts.
Think of Vault as a secured building with two entrances. Humans walk in through the front lobby, where the badge reader is wired straight into your corporate identity provider — there is no separate Vault password to remember or leak, just the SSO login your engineers already have. Workloads use the loading dock around back, where the only ID that counts is the service account token the pod was born with. Same building, two very different doors, and each needs a different lock. Here we wire up both: OIDC for people, Kubernetes auth for machines.
The front door: OIDC for humans
OIDC auth lets Vault delegate the question "who are you?" to an identity provider you already trust — Okta, Entra ID, Google, Keycloak. Vault never sees a password. It redirects the browser to the IdP, receives a signed ID token back, and reads claims out of it. You bind a role to the IdP's client ID and tell Vault which claim uniquely identifies the person (user_claim) and which claim carries their group membership (groups_claim). The allowed_redirect_uris must list both the CLI's localhost callback and your Vault UI address, exactly — scheme, port, and path included. Get one character wrong and the IdP refuses the handoff before Vault ever sees a token. Because the Vault token that comes out is short-lived, the human re-authenticates through SSO when it expires, so disabling their account upstream cuts off Vault on its own.
# Turn on the OIDC auth method
vault auth enable oidc
# Point Vault at your identity provider
vault write auth/oidc/config \
oidc_discovery_url="https://login.example.com" \
oidc_client_id="$OIDC_CLIENT_ID" \
oidc_client_secret="$OIDC_CLIENT_SECRET" \
default_role="engineer"
# A role: which claims to trust, where to redirect, what token to mint
vault write auth/oidc/role/engineer \
user_claim="sub" \
groups_claim="groups" \
oidc_scopes="openid,profile,groups" \
bound_audiences="$OIDC_CLIENT_ID" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" \
allowed_redirect_uris="https://vault.example.com/ui/vault/auth/oidc/oidc/callback" \
token_policies="engineer" \
token_ttl=1h
# Engineers now log in with: vault login -method=oidc role=engineer
Turning SSO groups into Vault policy
Granting policy to individual users does not scale and drifts the moment someone changes teams. Instead, let group membership in your IdP drive Vault authorization. When a user logs in through OIDC, Vault reads the groups_claim and looks for a matching group alias. You pre-create an external identity group, attach the policies you want it to carry, and map the IdP's group name to it through a group alias keyed on the OIDC mount accessor. Now a person added to platform-eng in Okta inherits the platform policy on their very next login, and removing them there revokes it — no Vault change, no ticket, no stale grant left behind. Membership lives in exactly one system, and Vault follows it. The alias name must match the string your IdP actually emits in the groups claim, which is often a group ID rather than a display name.
# Grab the accessor of the OIDC mount
ACCESSOR=$(vault auth list -format=json | jq -r '."oidc/".accessor')
# External group that carries the policy
vault write identity/group name="platform-eng" \
type="external" \
policies="platform"
GROUP_ID=$(vault read -field=id identity/group/name/platform-eng)
# Alias ties the IdP's group name to the Vault group
vault write identity/group-alias \
name="platform-eng" \
mount_accessor="$ACCESSOR" \
canonical_id="$GROUP_ID"
The loading dock: Kubernetes auth for workloads
Machines cannot do a browser redirect, so workloads authenticate with the one credential Kubernetes already hands every pod: its projected service account token. Vault takes that JWT, calls the cluster's TokenReview API to confirm it is genuine and unexpired, and — if the pod's service account name and namespace match a role's bindings — issues a Vault token in return. There is no secret to distribute and no static credential to rotate; the identity is the pod's own. Since Kubernetes 1.21 these tokens are time-bound and audience-scoped, so one scraped from a pod cannot be replayed forever or against a different audience. Bind each role narrowly to a service account name and namespace, so only the webapp pods in prod can ever assume the webapp policy — a compromised pod in another namespace gets nothing.
vault auth enable kubernetes
# In-cluster: you supply the host; Vault defaults to its own pod's
# service account token and the local cluster CA to reach the API
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
# Only the 'webapp' SA in 'prod' may assume this role
vault write auth/kubernetes/role/webapp \
bound_service_account_names="webapp" \
bound_service_account_namespaces="prod" \
token_policies="webapp" \
token_ttl=1h
# From inside the pod, exchange the SA token for a Vault token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
vault write auth/kubernetes/login role=webapp jwt="$TOKEN"
⚠️ TokenReview returns 403 and every pod login fails: With no token_reviewer_jwt on the mount, Vault does not review a pod's token with that same token — when Vault runs inside the cluster it uses its OWN pod's service account token to call the TokenReview API. So it is Vault's service account, not each application's, that must be bound to the system:auth-delegator ClusterRole. The Vault Helm chart wires this binding for the server service account by default, so in-cluster logins usually work out of the box — until someone overrides the chart's service account, or runs Vault outside the cluster. Out-of-cluster (or with disable_local_ca_jwt set) and still no reviewer JWT, Vault falls back to reviewing each login with the caller's own token, and now every authenticating service account needs the delegator — a brittle pattern. The durable fix is to set an explicit token_reviewer_jwt (a dedicated reviewer service account) so Vault always reviews with one privileged identity. Either way Kubernetes answers 403 as a misleading "permission denied" even when your role bindings are perfect, so check the RBAC before you burn an afternoon re-reading correct-looking bindings.
This lesson is part of the free *Vault from dev to production** course at SecOpsLog — hands-on, command-first DevSecOps tutorials. Read the original with the full interactive version →*
Top comments (0)