Vault Authentication - Auth Methods: Token, AppRole, LDAP, Kubernetes

Status: Active
Last Updated: 2026-08-14
Category: Security - Phase 4: Secret & Access Management
Prerequisites: vault-introduction, rbac-basics
Time: 3-4 hours
Tags: vault, authentication, approle, ldap, kubernetes, oidc, tokens

Summary

Getting a secret out of Vault requires proving who you are first. Vault supports pluggable auth methods β€” each one a different way for humans or machines to establish an identity and receive a token. This lesson covers the four you will actually use: human-friendly token auth and OIDC/LDAP for people, AppRole for servers and CI/CD pipelines, and Kubernetes auth for in-cluster workloads β€” including how to keep long-lived credentials out of your config files entirely.


🎯 What You'll Learn


How Auth Methods Work

Every request to Vault needs a token. Auth methods are factories for tokens:

LDAP creds ──┐
AppRole IDs ─┼──▢ [Auth Method] ──▢ Vault Token ──▢ Policies attached ──▢ secret access
K8s JWT ──────        (validates          (identity +      (least privilege,
User/pass β”€β”€β”€β”˜         creds)             TTL)             audit trail)

What Happens: you log in through a method (vault login -method=...), Vault validates the credential against the source system, builds a token with an identity and a set of policies, and hands it back. From then on, all requests carry X-Vault-Token. The token expires; re-authenticate when it does.

Enable any method under its mount point:

export VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=hvs.root-token

docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
  vault auth list

Method 1: Tokens (Baseline)

Vault's native method is always enabled (token/). You already used one: the initial root token.

Create a Scoped Token Instead of Using Root

vault token create -policy=app-readonly -ttl=1h -renewable=true

Output:

token                hvs.CAESIJ...
token_accessor       aBcDeF...
token_duration       1h
token_renewable      true
token_policies       ["app-readonly" "default"]

What Happens: the new token can do only what app-readonly allows (policies defined in rbac-basics), dies after an hour unless renewed, and can be revoked instantly:

vault token revoke hvs.CAESIJ...     # dead everywhere, immediately
vault token lookup hvs.CAESIJ...     # inspect TTL, policies, meta

Token rules of thumb:

Rule Why
Never ship the root token into apps Root bypasses ALL policy checks
Prefer short TTL + renewable Limits blast radius of leaks
Revoke on role change/offboarding Instant kill switch beats password rotation
Use batch tokens (stateless) for high-volume reads No lease tracking overhead

Method 2: AppRole (Machines, CI/CD)

The problem with static tokens for automation: if they leak, attackers get a working credential indefinitely. AppRole splits machine identity into two parts delivered through different channels:

Login requires both:

# Enable and define a role
vault auth enable approle

vault write auth/approle/role/ci-deployer \
    token_policies="app-deploy" \
    token_ttl=20m \
    token_max_ttl=1h \
    secret_id_ttl=24h \
    secret_id_num_uses=10

# Fetch the two values
vault read  -field=role_id   auth/approle/role/ci-deployer/role-id
vault write -field=secret_id auth/approle/role/ci-deployer/secret-id

What Happens: this role's tokens live at most 1h, and each SecretID works max 10 times within 24h β€” a leaked SecretID has a hard expiry even if nobody notices.

Consumer script (works on any server or CI runner):

#!/usr/bin/env bash
set -euo pipefail
ROLE_ID="$APPROLE_ROLE_ID"      # injected via CI variable / env file
SECRET_ID="$APPROLE_SECRET_ID"  # injected as a protected secret

VAULT_TOKEN=$(curl -s --request POST \
  --data "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \
  $VAULT_ADDR/v1/auth/approle/login | jq -r '.auth.client_token')

curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  "$VAULT_ADDR/v1/secret/data/prod/api-key" | jq -r '.data.data.key'

What Happens: the runner authenticates once, gets a ~20-minute token scoped to app-deploy, fetches exactly the secret it needs, and the token evaporates after the job. Nothing long-lived ever sits in CI variables except the SecretID β€” which self-expires.

Hardening options per role: bind_secret_id=true (default), CIDR restrictions (token_bound_cidrs), and wrapping responses (one-time-use delivery):

vault write -wrap-ttl=5m -field=wrapping_token \
  auth/approle/role/ci-deployer/secret-id
# deliver wrapping_token; consumer unwraps ONCE to reveal secret_id

Method 3: LDAP / OIDC (Humans)

Humans shouldn't manage individual Vault tokens. Point Vault at your central identity provider (identity-management covers standing up Keycloak/LDAP).

LDAP

vault auth enable ldap

vault write auth/ldap/config \
    url="ldaps://keycloak.internal:636" \
    userdn="ou=people,dc=fogserv,dc=cloud" \
    groupdn="ou=groups,dc=fogserv,dc=cloud" \
    binddn="cn=vault-reader,ou=service,dc=fogserv,dc=cloud" \
    bindpass="$LDAP_BINDPW"

# Map AD/LDAP groups β†’ Vault policies
vault write auth/ldap/groups/security-team policies=admin-audit
vault write auth/ldap/groups/developers   policies=dev-secrets
vault write auth/ldap/groups/break-glass  policies=root-manager

Then humans just run vault login -method=ldap username=jane β€” their group membership decides what they can touch. Offboard them in LDAP once and Vault access disappears automatically.

OIDC (Keycloak, Authentik, Google…)

vault auth enable oidc

vault write auth/oidc/config \
    oidc_discovery_url="https://sso.fogserv.cloud/realms/fogserv" \
    oidc_client_id="vault" \
    oidc_client_secret="$OIDC_CLIENT_SECRET" \
    default_role="user"

vault write auth/oidc/role/user \
    bound_audiences="vault" \
    allowed_redirect_uris="http://localhost:8250/oidc/callback" \
    token_policies="default" \
    oidc_scopes="openid,profile,groups"

vault login -method=oidc opens a browser, bounces through your SSO, done. Group→policy mapping works the same way via auth/oidc/groups/*.


Method 4: Kubernetes Auth (In-Cluster Workloads)

The gold standard for pods: authenticate using the pod's own service account JWT, which Kubernetes issues and rotates automatically. No secrets stored anywhere.

On the Kubernetes side, create the reviewer binding:

kubectl create serviceaccount api-service -n prod

On the Vault side (run where kubectl context points at the cluster):

vault auth enable kubernetes

vault write auth/kubernetes/config \
    kubernetes_host="https://k0s-api.internal:6443"

vault write auth/kubernetes/role/api-service \
    bound_service_account_names="api-service" \
    bound_service_account_namespaces="prod" \
    token_policies="api-service-secrets" \
    token_ttl=15m

What Happens: only pods running as SA api-service in namespace prod may exchange their JWT for a 15-minute Vault token scoped to api-service-secrets. Any other workload is rejected before touching secrets.

Pod consumption β€” the clean pattern is Vault Agent injecting secrets, but the direct version shows the mechanics:

apiVersion: v1
kind: Pod
metadata:
  name: api-service
  namespace: prod
spec:
  serviceAccountName: api-service     # ← this IS the credential
  containers:
    - name: app
      image: registry.internal/myapp:1.7.2
      env:
        - name: VAULT_ADDR
          value: "https://vault.internal:8200"
        - name: VAULT_SA_JWT           # auto-mounted by Kubernetes
          valueFrom:
            fieldRef:
              fieldPath: metadata.annotations['kubernetes.io/service-account/token']
      command: ["/bin/sh","-c"]
      args:
        - |
          VAULT_TOKEN=$(curl -s \
            --data "{\"jwt\":\"$VAULT_SA_JWT\",\"role\":\"api-service\"}" \
            $VAULT_ADDR/v1/auth/kubernetes/login | jq -r .auth.client_token)
          export DB_PASS=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
            $VAULT_ADDR/v1/secret/data/prod/db | jq -r .data.data.password)
          exec ./serve

For production use Vault Agent Injector or the CSI driver so templates render secrets into files/env without app-side code β€” same auth flow underneath.


Choosing the Right Method

Consumer Method Long-lived credential stored? Revocation story
Admin/human interactive OIDC or LDAP None (SSO) Disable in IdP
CI/CD pipeline AppRole (+wrapping) SecretID (short TTL, limited uses) Delete SecretID / revoke accessor
Server outside K8s AppRole SecretID Same
Pod in k0s/K8s Kubernetes auth None Delete SA / policy binding
Break-glass emergency Root token (offline) Physical safe N/A β€” used rarely

Anti-patterns to avoid: shipping root tokens in .env; sharing one AppRole among unrelated services (you lose per-app blast-radius isolation); leaving secret_id_ttl=0 (never expires).


Troubleshooting & Common Issues

Symptom Likely cause Fix
permission denied right after successful login Login β‰  authorization; token lacks policies vault token lookup β†’ check policies; add via role config
AppRole login fails with invalid role or secret ID SecretID expired or use-count exhausted Issue fresh SecretID; check secret_id_num_uses
K8s auth: service account name not authorized Mismatched SA name/namespace in role Compare exact strings in bound_service_account_*
LDAP login works, no permissions Groups not mapped to policies vault read auth/ldap/groups/<group>
OIDC redirect error Callback URI not in allow-list Add exact allowed_redirect_uris entry
Clock skew errors with JWT validation Host time drift > few minutes Run chrony/NTP everywhere (see ../basics/linux-fundamentals)

Debug sequence that solves most auth mysteries:

vault token lookup <token>          # who am I, what policies?
vault policy read <policy-name>     # what CAN this identity do?
vault audit list                    # then read audit logs for the actual denials

πŸ”— Related


Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse