Skip to main content

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

ServiceRolePolicy1Password Item
NornsnornsAPI keys, integrations, databaseOpenBao AppRole - Norns
n8nn8nIntegrations, API keys, databaseOpenBao AppRole - n8n
BifrostbifrostDatabase, LangfuseOpenBao AppRole - Bifrost
Voice Gatewayvoice-gatewayDeepgram, ElevenLabs, Twilio, LiveKitOpenBao 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:

  1. Policy is correctly defined
  2. Path in policy matches request (note: KV v2 uses secret/data/ prefix)
  3. 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

  1. Least Privilege: Create policies with minimum required access
  2. Rotate Secrets: Regularly rotate secret_ids
  3. Short TTLs: Use short token TTLs (1 hour default)
  4. Audit Logs: Review vault access logs periodically
  5. Secure Storage: Store credentials in 1Password, not in code