Vault Integration
Portoser integrates with HashiCorp Vault for centralized secret management, ensuring your sensitive data is secure and properly managed across all services.
Overview
Instead of storing secrets in:
- Environment files (
.env) - Configuration files
- Docker Compose files
- Command-line arguments
Portoser retrieves secrets from Vault at deployment time and injects them securely into your services.
Benefits
- Centralized Management: All secrets in one place
- Access Control: Per-machine and per-service permissions
- Audit Logging: Track who accessed what secrets when
- Secret Rotation: Update secrets without redeploying
- Encryption: Secrets encrypted at rest and in transit
- Compliance: Meet regulatory requirements
Prerequisites
1. Install Vault
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/vault
# Linux
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
2. Start Vault Server
# Development mode (NOT for production)
vault server -dev
# Production mode
vault server -config=/etc/vault/config.hcl
3. Initialize Vault
# Set Vault address
export VAULT_ADDR='http://127.0.0.1:8200'
# Initialize (production only)
vault operator init
# Unseal (production only)
vault operator unseal <key-1>
vault operator unseal <key-2>
vault operator unseal <key-3>
# Login
vault login <root-token>
Configuration
1. Configure Portoser
Set Vault address in your environment:
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='<your-token>'
Or in .env:
VAULT_ADDR=http://127.0.0.1:8200
VAULT_TOKEN=s.xxxxxxxxxxxxxxxxxxxxxxxx
2. Enable AppRole Authentication
AppRole allows each machine to authenticate with its own credentials:
# Enable AppRole
vault auth enable approle
# Create policy for a machine
vault policy write m1-policy - << EOF
path "secret/data/services/*" {
capabilities = ["read", "list"]
}
EOF
# Create AppRole
vault write auth/approle/role/m1 \
token_policies="m1-policy" \
token_ttl=1h \
token_max_ttl=4h
# Get role ID
vault read auth/approle/role/m1/role-id
# Get secret ID
vault write -f auth/approle/role/m1/secret-id
3. Configure Machine in Registry
machines:
m1:
host: "192.168.0.245"
user: "admin"
platform: "darwin"
vault_role: "m1"
vault_role_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
vault_secret_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Storing Secrets
Using Vault CLI
# Store a secret
vault kv put secret/services/my-app \
api_key="sk-1234567890abcdef" \
database_url="postgresql://user:pass@host:5432/db" \
jwt_secret="my-super-secret-key"
# Read a secret
vault kv get secret/services/my-app
# Update a secret
vault kv put secret/services/my-app api_key="new-key"
# Delete a secret
vault kv delete secret/services/my-app
Using Portoser CLI
# Store a secret
portoser vault set my-app api_key "sk-1234567890abcdef"
# Get a secret
portoser vault get my-app api_key
# List secrets for a service
portoser vault list my-app
# Delete a secret
portoser vault delete my-app api_key
Using Secrets in Services
In Registry Configuration
services:
my-app:
type: "docker"
path: "/opt/my-app"
machine: "m1"
vault_secrets:
- path: "secret/data/services/my-app"
keys:
- api_key
- database_url
- jwt_secret
When deployed, Portoser will:
- Authenticate to Vault using the machine's AppRole
- Fetch the specified secrets
- Inject them as environment variables:
API_KEY=sk-1234567890abcdefDATABASE_URL=postgresql://...JWT_SECRET=my-super-secret-key
Custom Environment Variable Names
services:
my-app:
vault_secrets:
- path: "secret/data/services/my-app"
mapping:
api_key: "MY_CUSTOM_API_KEY"
database_url: "DB_CONNECTION_STRING"
Results in:
MY_CUSTOM_API_KEY=sk-1234567890abcdefDB_CONNECTION_STRING=postgresql://...
Multiple Secret Paths
services:
my-app:
vault_secrets:
- path: "secret/data/services/my-app"
keys: ["api_key", "jwt_secret"]
- path: "secret/data/shared/database"
keys: ["url", "username", "password"]
- path: "secret/data/shared/smtp"
keys: ["host", "port", "password"]
Secret Rotation
Manual Rotation
# Update secret in Vault
vault kv put secret/services/my-app \
api_key="new-key-value"
# Restart service to pick up new secret
portoser restart my-app
Automatic Rotation
Enable automatic secret rotation:
services:
my-app:
vault_secrets:
- path: "secret/data/services/my-app"
keys: ["api_key"]
auto_rotate: true
rotation_interval: "24h"
Portoser will:
- Check for secret changes every 24 hours
- Restart the service if secrets have changed
- Log rotation events
Access Control
Per-Machine Policies
Create policies that limit what each machine can access:
# Policy for machine m1
vault policy write m1-policy - << EOF
# Can read all service secrets
path "secret/data/services/*" {
capabilities = ["read", "list"]
}
# Cannot read admin secrets
path "secret/data/admin/*" {
capabilities = ["deny"]
}
EOF
# Policy for machine mini1 (limited access)
vault policy write mini1-policy - << EOF
# Can only read specific service secrets
path "secret/data/services/web-app" {
capabilities = ["read"]
}
path "secret/data/services/api" {
capabilities = ["read"]
}
EOF
Per-Service Policies
Limit which services can access which secrets:
vault policy write web-app-policy - << EOF
# Web app can only read its own secrets
path "secret/data/services/web-app" {
capabilities = ["read"]
}
# And shared database secrets
path "secret/data/shared/database" {
capabilities = ["read"]
}
EOF
Advanced Features
Dynamic Secrets
Generate secrets on-demand:
# Enable database secrets engine
vault secrets enable database
# Configure database connection
vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
allowed_roles="*" \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/postgres" \
username="vault" \
password="vaultpass"
# Create role for dynamic credentials
vault write database/roles/my-app \
db_name=postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Use in service:
services:
my-app:
vault_dynamic:
- engine: "database"
role: "my-app"
env_prefix: "DB"
Certificate Management
Store TLS certificates in Vault:
# Store certificate
vault kv put secret/certs/my-app \
cert=@/path/to/cert.pem \
key=@/path/to/key.pem \
ca=@/path/to/ca.pem
Use in service:
services:
my-app:
vault_certs:
path: "secret/data/certs/my-app"
cert_file: "/etc/ssl/my-app.crt"
key_file: "/etc/ssl/my-app.key"
Secret Versioning
Vault keeps previous versions of secrets:
# View secret history
vault kv get -version=1 secret/services/my-app
vault kv get -version=2 secret/services/my-app
# Rollback to previous version
vault kv rollback -version=1 secret/services/my-app
# Delete specific version
vault kv delete -versions=2 secret/services/my-app
# Destroy version permanently
vault kv destroy -versions=2 secret/services/my-app
Best Practices
1. Use AppRole, Not Root Token
# Bad - using root token
export VAULT_TOKEN='s.root-token'
# Good - using AppRole per machine
# In registry.yml:
machines:
m1:
vault_role_id: "..."
vault_secret_id: "..."
2. Rotate Secrets Regularly
# Set up rotation schedule
# Every 90 days for API keys
# Every 30 days for database passwords
# Every 7 days for session secrets
3. Use Least Privilege
# Only grant necessary permissions
vault policy write restrictive - << EOF
path "secret/data/services/my-app" {
capabilities = ["read"]
}
# No write, no delete, no list
EOF
4. Enable Audit Logging
vault audit enable file file_path=/var/log/vault-audit.log
5. Never Commit Secrets to Git
# Add to .gitignore
.env
.env.local
vault-token
secret_id
6. Use Namespaces (Vault Enterprise)
# Create namespace per environment
vault namespace create production
vault namespace create staging
vault namespace create development
Troubleshooting
Can't Connect to Vault
# Check Vault status
vault status
# Check connectivity
curl $VAULT_ADDR/v1/sys/health
# Verify address
echo $VAULT_ADDR
Authentication Failed
# Check token validity
vault token lookup
# Check AppRole credentials
vault write auth/approle/login \
role_id="..." \
secret_id="..."
# Renew token
vault token renew
Secret Not Found
# List available secrets
vault kv list secret/services/
# Check secret path
vault kv get secret/services/my-app
# Verify permissions
vault token capabilities secret/data/services/my-app
Permission Denied
# Check token policies
vault token lookup
# Check policy permissions
vault policy read my-policy
# Test capabilities
vault token capabilities secret/data/services/my-app
Security Considerations
1. Network Security
- Use TLS for Vault connections in production
- Configure firewall rules to limit Vault access
- Use private networks when possible
2. Token Management
- Set appropriate TTLs for tokens
- Implement token renewal
- Revoke tokens when no longer needed
3. Secret Storage
- Use KV v2 for versioning
- Enable audit logging
- Regular backup of Vault data
4. Monitoring
- Monitor Vault access logs
- Alert on unusual access patterns
- Track secret rotation
Next Steps
- Certificates & mTLS — managing TLS certificates and the cluster CA
- Service Registry — configure Vault references in the registry
- Caddy Integration — how Caddy uses certs Vault helps protect