OpenBao AppRole Authentication
Service-to-service authentication for accessing OpenBao secrets.
Overview
AppRole is a machine-oriented authentication method for services to obtain vault tokens. Each service has a Role ID (public identifier) and Secret ID (private credential) that together authenticate and return a token with the appropriate policy.
Configured AppRoles
| Service | Role | Policy | 1Password Item |
|---|---|---|---|
| Norns | norns | API keys, integrations, database | OpenBao AppRole - Norns |
| n8n | n8n | Integrations, API keys, database | OpenBao AppRole - n8n |
| Bifrost | bifrost | Database, Langfuse | OpenBao AppRole - Bifrost |
| Voice Gateway | voice-gateway | Deepgram, ElevenLabs, Twilio, LiveKit | OpenBao AppRole - Voice Gateway |
Policy Permissions
Norns Policy
path "secret/data/api-keys/*" { capabilities = ["read"] }
path "secret/data/integrations/*" { capabilities = ["read"] }
path "secret/data/database/postgres" { capabilities = ["read"] }
path "secret/data/database/docs/infrastructure/redis" { capabilities = ["read"] }
n8n Policy
path "secret/data/integrations/*" { capabilities = ["read"] }
path "secret/data/api-keys/*" { capabilities = ["read"] }
path "secret/data/database/*" { capabilities = ["read"] }
Bifrost Policy
path "secret/data/database/postgres" { capabilities = ["read"] }
path "secret/data/database/docs/infrastructure/redis" { capabilities = ["read"] }
path "secret/data/services/langfuse" { capabilities = ["read"] }
Voice Gateway Policy
path "secret/data/api-keys/deepgram" { capabilities = ["read"] }
path "secret/data/api-keys/elevenlabs" { capabilities = ["read"] }
path "secret/data/integrations/twilio" { capabilities = ["read"] }
path "secret/data/services/livekit" { capabilities = ["read"] }
Using AppRole in Services
Step 1: Get Credentials from 1Password
ROLE_ID=$(op item get "OpenBao AppRole - Norns" --vault ravenmask --fields role_id --reveal)
SECRET_ID=$(op item get "OpenBao AppRole - Norns" --vault ravenmask --fields secret_id --reveal)
Step 2: Authenticate to Get Token
# Login with AppRole
TOKEN=$(curl -s -X POST \
https://vault.ravenhelm.dev/v1/auth/approle/login \
-d "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \
| jq -r '.auth.client_token')
Step 3: Read Secrets
# Read a secret
curl -s -H "X-Vault-Token: $TOKEN" \
https://vault.ravenhelm.dev/v1/secret/data/api-keys/anthropic \
| jq -r '.data.data.api_key'
Python Integration Example
import hvac
import os
# Initialize client
client = hvac.Client(url='https://vault.ravenhelm.dev')
# Authenticate with AppRole
client.auth.approle.login(
role_id=os.environ['VAULT_ROLE_ID'],
secret_id=os.environ['VAULT_SECRET_ID']
)
# Read a secret
secret = client.secrets.kv.v2.read_secret_version(
path='api-keys/anthropic',
mount_point='secret'
)
api_key = secret['data']['data']['api_key']
Node.js Integration Example
const vault = require('node-vault')({
apiVersion: 'v1',
endpoint: 'https://vault.ravenhelm.dev'
});
async function getSecret(path) {
// Login with AppRole
const auth = await vault.approleLogin({
role_id: process.env.VAULT_ROLE_ID,
secret_id: process.env.VAULT_SECRET_ID
});
vault.token = auth.auth.client_token;
// Read secret
const result = await vault.read(`secret/data/${path}`);
return result.data.data;
}
// Usage
const apiKey = await getSecret('api-keys/anthropic');
Bash/Shell Integration
#!/bin/bash
VAULT_ADDR="https://vault.ravenhelm.dev"
# Authenticate
AUTH_RESPONSE=$(curl -s -X POST \
"${VAULT_ADDR}/v1/auth/approle/login" \
-d "{\"role_id\":\"${VAULT_ROLE_ID}\",\"secret_id\":\"${VAULT_SECRET_ID}\"}")
VAULT_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.auth.client_token')
# Function to fetch secret
fetch_secret() {
local path=$1
local field=$2
curl -s -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/${path}" | \
jq -r ".data.data.${field}"
}
# Usage
ANTHROPIC_KEY=$(fetch_secret 'api-keys/anthropic' 'api_key')
Docker Compose Integration
Add to service environment:
services:
myservice:
environment:
- VAULT_ADDR=https://vault.ravenhelm.dev
- VAULT_ROLE_ID=${VAULT_ROLE_ID}
- VAULT_SECRET_ID=${VAULT_SECRET_ID}
Add to .env for the service:
VAULT_ROLE_ID=$(op item get "OpenBao AppRole - MyService" --vault ravenmask --fields role_id --reveal)
VAULT_SECRET_ID=$(op item get "OpenBao AppRole - MyService" --vault ravenmask --fields secret_id --reveal)
Token Renewal
AppRole tokens have a TTL of 1 hour (max 4 hours). Services should either:
Option 1: Re-authenticate
Get a new token when the current one expires.
Option 2: Renew Before Expiry
curl -s -X POST \
-H "X-Vault-Token: $TOKEN" \
https://vault.ravenhelm.dev/v1/auth/token/renew-self
Token Lookup
Check current token status:
curl -s -H "X-Vault-Token: $TOKEN" \
https://vault.ravenhelm.dev/v1/auth/token/lookup-self | jq '.data.ttl'
Creating New AppRoles
ROOT_TOKEN=$(op item get "OpenBao Root Keys" --vault ravenmask --fields "Root Token" --reveal)
# 1. Create policy file
cat > /tmp/myservice.hcl << 'EOF'
path "secret/data/specific/path" {
capabilities = ["read"]
}
EOF
# 2. Upload policy
scp /tmp/myservice.hcl ravenhelm@100.115.101.81:/tmp/
ssh ravenhelm@100.115.101.81 "docker cp /tmp/myservice.hcl openbao:/tmp/"
ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao policy write myservice /tmp/myservice.hcl"
# 3. Create role
ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao write auth/approle/role/myservice \
token_policies=myservice \
token_ttl=1h \
token_max_ttl=4h"
# 4. Get credentials
ROLE_ID=$(ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao read -field=role_id auth/approle/role/myservice/role-id")
SECRET_ID=$(ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao write -field=secret_id -f auth/approle/role/myservice/secret-id")
# 5. Store in 1Password
op item create --vault ravenmask --category "API Credential" --title "OpenBao AppRole - MyService" \
"role_id=$ROLE_ID" \
"secret_id[password]=$SECRET_ID" \
"vault_addr=https://vault.ravenhelm.dev"
Rotating Secret IDs
Secret IDs can be rotated without changing the role:
# Generate new secret ID
NEW_SECRET=$(ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao write -field=secret_id -f auth/approle/role/myservice/secret-id")
# Update 1Password
op item edit "OpenBao AppRole - MyService" --vault ravenmask "secret_id=$NEW_SECRET"
# Update service .env and restart
Troubleshooting
"permission denied" Errors
The token doesn't have access to the requested path. Check:
- Policy is correctly defined
- Path in policy matches request (note: KV v2 uses
secret/data/prefix) - Role has the correct policy attached
# Check role's policies
ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao read auth/approle/role/myservice"
"token expired" Errors
Token TTL exceeded. Re-authenticate with AppRole.
"invalid role or secret ID"
Credentials are wrong or secret ID was revoked. Generate new secret ID:
NEW_SECRET=$(ssh ravenhelm@100.115.101.81 "docker exec -e BAO_TOKEN=$ROOT_TOKEN openbao bao write -field=secret_id -f auth/approle/role/myservice/secret-id")
"no route to host" / Connection Errors
Check network connectivity to vault.ravenhelm.dev from the service container.
Security Best Practices
- Least Privilege: Create policies with minimum required access
- Rotate Secrets: Regularly rotate secret_ids
- Short TTLs: Use short token TTLs (1 hour default)
- Audit Logs: Review vault access logs periodically
- Secure Storage: Store credentials in 1Password, not in code
Related Documentation
- OpenBao - Main documentation
- OpenBao Norns Integration - Norns vault integration
- Secrets Migration - Migration plan