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

  1. Limit SecretID uses — Set secret_id_num_uses=1 for jobs that authenticate once
  2. Short TTLs — Keep token_ttl as short as your workload allows
  3. 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"
    
  4. Audit everything — Enable Vault audit logs to track all AppRole authentications
  5. 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.