This is part of the Vault + Kubernetes Integration Guide. Return to the main guide for the full architecture overview.
AppRole is a machine-oriented auth method. Unlike Kubernetes Auth (which requires Vault to call the K8s TokenReview API), AppRole uses a RoleID + SecretID pair — making it ideal when Vault cannot reach the Kubernetes API server.
When to Use AppRole Over Kubernetes Auth
| Scenario | Use AppRole? |
|---|---|
| Vault is external and cannot reach K8s API | ✅ Yes |
| CI/CD pipelines (Jenkins, GitLab CI, GitHub Actions) | ✅ Yes |
| Batch jobs or CronJobs needing Vault access | ✅ Consider |
| Cross-cloud authentication | ✅ Yes |
| Vault is in-cluster or can reach K8s API | ❌ Use K8s Auth |
How AppRole Works
┌────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────┐ ┌──────────────────┐ │
│ │ App Pod │ 1. Login with │ Vault Server │ │
│ │ │ RoleID+SecretID │ │ │
│ │ ┌──────┐ │─────────────────────►│ AppRole Auth │ │
│ │ │Secret│ │ │ Engine │ │
│ │ │Mount │ │ 2. Returns Vault │ │ │
│ │ └──────┘ │ token │ ┌────────────┐ │ │
│ │ │◄─────────────────────│ │ KV Secrets │ │ │
│ └────────────┘ 3. Read secrets │ │ DB Creds │ │ │
│ using token │ │ PKI Certs │ │ │
│ ─────────────────────►│ └────────────┘ │ │
│ └──────────────────┘ │
└────────────────────────────────────────────────────────────┘
- RoleID — A static identifier (like a username). Safe to embed in config.
- SecretID — A sensitive credential (like a password). Must be protected.
Step-by-Step Implementation
Step 1: Enable AppRole in Vault
export VAULT_ADDR="https://vault.corp.com:8200"
vault auth enable approle
Step 2: Create a Vault Policy
vault policy write webapp-approle-policy - <<EOF
# KV v2 read access
path "secret/data/webapp/*" {
capabilities = ["read", "list"]
}
path "secret/metadata/webapp/*" {
capabilities = ["read", "list"]
}
# Dynamic database credentials
path "database/creds/webapp-db" {
capabilities = ["read"]
}
EOF
Step 3: Create the AppRole
vault write auth/approle/role/webapp-role \
token_policies="webapp-approle-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=720h \
secret_id_num_uses=0 \
token_num_uses=0 \
token_type=service
Key parameters:
| Parameter | Value | Description |
|---|---|---|
secret_id_ttl |
720h (30 days) |
How long the SecretID is valid |
secret_id_num_uses |
0 (unlimited) |
Set to 1 for single-use (more secure) |
token_ttl |
1h |
Vault token lifetime |
token_max_ttl |
4h |
Maximum renewable lifetime |
Step 4: Retrieve RoleID and Generate SecretID
# Get RoleID (not sensitive — can be baked into config)
ROLE_ID=$(vault read -field=role_id auth/approle/role/webapp-role/role-id)
echo "RoleID: $ROLE_ID"
# Generate SecretID (sensitive — treat like a password)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/webapp-role/secret-id)
echo "SecretID: $SECRET_ID"
Step 5: Store Credentials in Kubernetes
kubectl create namespace webapp-ns
kubectl create secret generic vault-approle-creds \
-n webapp-ns \
--from-literal=role-id="$ROLE_ID" \
--from-literal=secret-id="$SECRET_ID"
Step 6: Test Authentication
# Login with AppRole
vault write auth/approle/login \
role_id="$ROLE_ID" \
secret_id="$SECRET_ID"
# Expected output:
# Key Value
# token hvs.CAESI...
# token_policies ["default" "webapp-approle-policy"]
Integration Pattern 1: Vault Agent Sidecar with AppRole
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: webapp-ns
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: my-webapp:latest
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
- name: vault-agent
image: hashicorp/vault:1.17
args: ["agent", "-config=/etc/vault/agent-config.hcl"]
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault
- name: vault-secrets
mountPath: /vault/secrets
- name: approle-creds
mountPath: /vault/approle
readOnly: true
volumes:
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: vault-secrets
emptyDir:
medium: Memory
- name: approle-creds
secret:
secretName: vault-approle-creds
---
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-agent-config
namespace: webapp-ns
data:
agent-config.hcl: |
vault {
address = "https://vault.corp.com:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/vault/approle/role-id"
secret_id_file_path = "/vault/approle/secret-id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/vault/secrets/.vault-token"
}
}
}
template {
contents = <<-EOT
{{ with secret "secret/data/webapp/config" }}
DB_HOST={{ .Data.data.db_host }}
DB_USER={{ .Data.data.db_user }}
DB_PASS={{ .Data.data.db_pass }}
{{ end }}
EOT
destination = "/vault/secrets/app.env"
}
Integration Pattern 2: External Secrets Operator (ESO)
ESO syncs Vault secrets into native Kubernetes Secrets using CRDs:
# Install ESO
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace
# ClusterSecretStore — reusable across namespaces
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-approle
spec:
provider:
vault:
server: "https://vault.corp.com:8200"
path: "secret"
version: "v2"
auth:
appRole:
path: "approle"
roleRef:
name: vault-approle-creds
namespace: webapp-ns
key: role-id
secretRef:
name: vault-approle-creds
namespace: webapp-ns
key: secret-id
---
# ExternalSecret — declares what to sync
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: webapp-config
namespace: webapp-ns
spec:
refreshInterval: 5m
secretStoreRef:
name: vault-approle
kind: ClusterSecretStore
target:
name: webapp-k8s-secret
creationPolicy: Owner
data:
- secretKey: DB_HOST
remoteRef:
key: secret/data/webapp/config
property: db_host
- secretKey: DB_PASS
remoteRef:
key: secret/data/webapp/config
property: db_pass
Your app consumes webapp-k8s-secret like any standard Kubernetes Secret.
SecretID Rotation Strategy
SecretIDs should be rotated regularly. Here’s a CronJob approach:
apiVersion: batch/v1
kind: CronJob
metadata:
name: rotate-vault-secret-id
namespace: webapp-ns
spec:
schedule: "0 0 */7 * *" # Weekly
jobTemplate:
spec:
template:
spec:
serviceAccountName: secret-rotator
containers:
- name: rotator
image: hashicorp/vault:1.17
command: ["/bin/sh", "-c"]
args:
- |
export VAULT_ADDR="https://vault.corp.com:8200"
# Login with current creds
VAULT_TOKEN=$(vault write -field=token auth/approle/login \
role_id="$(cat /vault/approle/role-id)" \
secret_id="$(cat /vault/approle/secret-id)")
export VAULT_TOKEN
# Generate new SecretID
NEW_SECRET=$(vault write -f -field=secret_id \
auth/approle/role/webapp-role/secret-id)
# Update K8s secret (requires RBAC)
kubectl create secret generic vault-approle-creds \
-n webapp-ns \
--from-literal=role-id="$(cat /vault/approle/role-id)" \
--from-literal=secret-id="$NEW_SECRET" \
--dry-run=client -o yaml | kubectl apply -f -
volumeMounts:
- name: approle-creds
mountPath: /vault/approle
readOnly: true
volumes:
- name: approle-creds
secret:
secretName: vault-approle-creds
restartPolicy: OnFailure
Security Best Practices
- Limit SecretID uses — Set
secret_id_num_uses=1for jobs that authenticate once - Short TTLs — Keep
token_ttlas short as your workload allows - CIDR binding — Restrict which IPs can use the AppRole:
vault write auth/approle/role/webapp-role \ secret_id_bound_cidrs="10.0.0.0/8" \ token_bound_cidrs="10.0.0.0/8" - Audit everything — Enable Vault audit logs to track all AppRole authentications
- Never commit SecretIDs — Treat them like passwords; use K8s Secrets or a sealed-secrets workflow
Next: Learn how secrets are delivered to pods with the Vault Agent Injector Guide or the Vault Secrets Operator Guide.