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:

  1. Authenticate to Vault using the machine's AppRole
  2. Fetch the specified secrets
  3. Inject them as environment variables:
    • API_KEY=sk-1234567890abcdef
    • DATABASE_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-1234567890abcdef
  • DB_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