HashiCorp Vault Setup

Secure secret management for PiSovereign using HashiCorp Vault

Vault is included in the Docker Compose stack and initialized automatically on first run. This guide covers how secrets are structured, how to store them, and how Vault integrates with PiSovereign.

Overview

HashiCorp Vault provides centralized secret management with encryption at rest and in transit, fine-grained access control, audit logging, and secret rotation. PiSovereign’s Docker Compose setup includes Vault with automatic initialization via the vault-init sidecar container.

How It Works

┌─────────────────────────────────────────────────────┐
│                    PiSovereign                       │
│  ┌─────────────────────────────────────────────┐   │
│  │           ChainedSecretStore                 │   │
│  │  ┌─────────────┐    ┌──────────────────┐   │   │
│  │  │ VaultSecret │ →  │ EnvironmentSecret │   │   │
│  │  │   Store     │    │     Store         │   │   │
│  │  └─────────────┘    └──────────────────┘   │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────┐
│                 HashiCorp Vault                      │
│  ┌──────────────┐  ┌─────────────┐  ┌───────────┐ │
│  │ KV v2 Engine │  │   AppRole   │  │  Audit    │ │
│  │              │  │    Auth     │  │   Log     │ │
│  └──────────────┘  └─────────────┘  └───────────┘ │
└─────────────────────────────────────────────────────┘

PiSovereign uses a ChainedSecretStore that tries multiple backends in order:

  1. Vault (primary) — Production secrets stored securely
  2. Environment variables (fallback) — Overrides for development or CI

Initialization

Vault is initialized on first deployment via the Docker Compose init container. Run manually if needed:

cd docker
docker compose exec vault /vault/init.sh

Important: Save the unseal key and root token printed to stdout. Loss of the unseal key means loss of access to secrets.

After a container restart, Vault may need to be unsealed:

docker compose exec vault vault operator unseal <UNSEAL_KEY>

Storing Secrets

Store integration credentials in Vault after initialization:

# Enter the Vault container
docker compose exec vault sh

# WhatsApp credentials
vault kv put secret/pisovereign/whatsapp \
    access_token="your-meta-access-token" \
    app_secret="your-app-secret"

# Email credentials (IMAP/SMTP password or Bridge password)
vault kv put secret/pisovereign/email \
    password="your-email-password"

# CalDAV credentials
vault kv put secret/pisovereign/caldav \
    username="your-username" \
    password="your-password"

# OpenAI API key (for speech fallback)
vault kv put secret/pisovereign/openai \
    api_key="sk-your-openai-key"

# Brave Search API key
vault kv put secret/pisovereign/websearch \
    brave_api_key="BSA-your-key"

# Signal phone number
vault kv put secret/pisovereign/signal \
    phone_number="+491701234567"

# Verify a secret
vault kv get secret/pisovereign/whatsapp

Secret Paths

PiSovereign expects secrets at these paths:

SecretVault PathEnvironment Variable Fallback
WhatsApp Access Tokensecret/pisovereign/whatsappaccess_tokenPISOVEREIGN_WHATSAPP_ACCESS_TOKEN
WhatsApp App Secretsecret/pisovereign/whatsappapp_secretPISOVEREIGN_WHATSAPP_APP_SECRET
Email Passwordsecret/pisovereign/emailpasswordPISOVEREIGN_EMAIL_PASSWORD
CalDAV Usernamesecret/pisovereign/caldavusernamePISOVEREIGN_CALDAV_USERNAME
CalDAV Passwordsecret/pisovereign/caldavpasswordPISOVEREIGN_CALDAV_PASSWORD
OpenAI API Keysecret/pisovereign/openaiapi_keyPISOVEREIGN_OPENAI_API_KEY
Brave Search Keysecret/pisovereign/websearchbrave_api_keyPISOVEREIGN_WEBSEARCH_BRAVE_API_KEY
Signal Phone Numbersecret/pisovereign/signalphone_numberPISOVEREIGN_SIGNAL__PHONE_NUMBER

AppRole Authentication

For production, use AppRole instead of the root token. AppRole provides short-lived tokens with scoped permissions.

Create Policy

docker compose exec vault sh

vault policy write pisovereign - <<EOF
path "secret/data/pisovereign/*" {
  capabilities = ["read"]
}
path "secret/metadata/pisovereign/*" {
  capabilities = ["list"]
}
path "auth/token/renew-self" {
  capabilities = ["update"]
}
EOF

Configure AppRole

vault auth enable approle

vault write auth/approle/role/pisovereign \
    token_policies="pisovereign" \
    token_ttl=1h \
    token_max_ttl=4h \
    secret_id_ttl=720h \
    secret_id_num_uses=0

# Get Role ID
vault read auth/approle/role/pisovereign/role-id

# Generate Secret ID
vault write -f auth/approle/role/pisovereign/secret-id

Then configure PiSovereign to use AppRole in config.toml:

[vault]
address = "http://vault:8200"
role_id = "12345678-1234-1234-1234-123456789012"
secret_id = "abcd1234-abcd-1234-abcd-abcd12345678"
mount_path = "secret"
timeout_secs = 5

Tip: Store secret_id as an environment variable rather than in the config file:

export PISOVEREIGN_VAULT_SECRET_ID="abcd1234-..."

Operations

Secret Rotation

Update a secret without downtime — PiSovereign reads the latest version automatically:

vault kv put secret/pisovereign/whatsapp \
    access_token="new-access-token" \
    app_secret="same-app-secret"

View secret versions or rollback:

vault kv metadata get secret/pisovereign/whatsapp
vault kv rollback -version=2 secret/pisovereign/whatsapp

Backup

# Backup Vault data volume
docker run --rm -v docker_vault-data:/data -v $(pwd):/backup \
  alpine tar czf /backup/vault-backup-$(date +%Y%m%d).tar.gz /data

For disaster recovery, ensure you have the unseal key and root token stored securely in a separate location.


Troubleshooting

Cannot connect to Vault

docker compose exec vault vault status
docker compose logs vault

Permission denied

# Verify the token has the correct policy
docker compose exec vault vault token lookup
docker compose exec vault vault policy read pisovereign

Secret not found

# Verify the secret exists
docker compose exec vault vault kv get secret/pisovereign/whatsapp

# Check the mount path
docker compose exec vault vault secrets list

Vault sealed after restart

docker compose exec vault vault operator unseal <UNSEAL_KEY>

Next Steps