Skip to main content

Vaultwarden Advanced Setup: Security Hardening and Teams 2026

·OSSAlt Team
vaultwardenbitwardenpassword-managersecurityself-hostingdocker2026

TL;DR

Vaultwarden (AGPL 3.0, ~40K GitHub stars) goes beyond a basic password manager when properly configured — hardened with fail2ban, rate limiting, Duo 2FA enforcement, and PostgreSQL. This guide covers the production setup: switching from SQLite to PostgreSQL for reliability, enforcing 2FA organization-wide, setting up audit logs, configuring fail2ban, and running a family or team vault with proper access controls. See the basic setup guide first.

Key Takeaways

  • Production backend: Switch from SQLite to PostgreSQL for better performance and reliability
  • Fail2ban: Block IPs after failed login attempts
  • 2FA enforcement: Require all users to have 2FA enabled
  • Audit logs: Track what happened to your vault
  • Organization hardening: Manage teams with collections and role-based access
  • Monitoring: Healthcheck endpoints and Prometheus metrics

Part 1: PostgreSQL Backend

For multi-user or production use, switch from SQLite to PostgreSQL:

# docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - vaultwarden_data:/data
    environment:
      DOMAIN: "https://vault.yourdomain.com"
      DATABASE_URL: "postgresql://vaultwarden:${DB_PASSWORD}@db:5432/vaultwarden"
      SIGNUPS_ALLOWED: "false"
      ADMIN_TOKEN: "${ADMIN_TOKEN}"
      # Rate limiting:
      LOGIN_RATELIMIT_MAX_BURST: 10
      LOGIN_RATELIMIT_SECONDS: 60
      ADMIN_RATELIMIT_MAX_BURST: 5
      ADMIN_RATELIMIT_SECONDS: 300
      # 2FA enforcement:
      REQUIRE_DEVICE_EMAIL: "true"    # Require email 2FA if no other 2FA
      # Push notifications:
      PUSH_ENABLED: "true"
      PUSH_INSTALLATION_ID: "${PUSH_INSTALLATION_ID}"
      PUSH_INSTALLATION_KEY: "${PUSH_INSTALLATION_KEY}"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: vaultwarden
      POSTGRES_USER: vaultwarden
      POSTGRES_PASSWORD: "${DB_PASSWORD}"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vaultwarden"]
      interval: 10s
      start_period: 20s

volumes:
  vaultwarden_data:
  postgres_data:

Migrate from SQLite to PostgreSQL

# 1. Backup SQLite data first:
docker exec vaultwarden sqlite3 /data/db.sqlite3 ".backup /data/db-backup.sqlite3"

# 2. Start new Postgres-backed Vaultwarden
# (data directory will be empty — all users need to re-enroll, OR use migration)

# 3. Use vaultwarden-postgres-migration tool:
# https://github.com/pgerber/vaultwarden-postgres-migration
docker run --rm \
  -e SQLITE_URI=/data/db.sqlite3 \
  -e POSTGRES_URI="postgresql://vaultwarden:password@db:5432/vaultwarden" \
  -v vaultwarden_data:/data \
  ghcr.io/pgerber/vaultwarden-postgres-migration

Part 2: Fail2ban Integration

Block IPs after repeated failed login attempts:

# Add to docker-compose.yml:
services:
  fail2ban:
    image: crazymax/fail2ban:latest
    container_name: fail2ban
    restart: unless-stopped
    network_mode: host
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - /var/log:/var/log:ro
      - ./fail2ban:/data
# ./fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
action = iptables-allports[name=vaultwarden, chain=FORWARD]
logpath = /var/log/docker/vaultwarden.log
maxretry = 5
bantime = 3600
findtime = 600

[vaultwarden-admin]
enabled = true
port = 80,443
filter = vaultwarden-admin
action = iptables-allports[name=vaultwarden-admin, chain=FORWARD]
logpath = /var/log/docker/vaultwarden.log
maxretry = 3
bantime = 86400
findtime = 600
# ./fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>.*$
            ^.*Failed login attempt for .* from <ADDR>.*$
ignoreregex =

# ./fail2ban/filter.d/vaultwarden-admin.conf
[Definition]
failregex = ^.*ADMIN-TOKEN.*Invalid admin token\. IP: <ADDR>.*$
ignoreregex =

Part 3: Enforce 2FA Organization-Wide

Require all users to enable 2FA:

environment:
  # Require 2FA — users without it are blocked until they enroll
  REQUIRE_DEVICE_EMAIL: "true"

Via the Admin Panel (/admin):

  1. Users → select user → Enable 2FA Required
  2. Or in Organization settings → Require 2FA for all members

Users without 2FA see a mandatory enrollment prompt on next login.

Supported 2FA methods:

  • TOTP (Google Authenticator, Aegis, Bitwarden Authenticator)
  • YubiKey OTP
  • FIDO2/WebAuthn (hardware security keys, passkeys)
  • Email OTP (fallback)
  • Duo

Part 4: Security Headers

Add security headers via Caddy:

vault.yourdomain.com {
    header {
        # Security headers:
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "interest-cohort=()"
        # Content Security Policy:
        Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https://push.bitwarden.com https://notifications.bitwarden.com; frame-src 'self'"
    }
    reverse_proxy localhost:8080
}

Part 5: Email Verification and Invitations

When signups are disabled, manage users via invitations:

# Invite a user via admin panel:
# Admin → Users → Invite User → enter email

# Or via CLI:
docker exec vaultwarden curl -X POST http://localhost:80/api/organizations/ORG_ID/users/invite \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ADMIN_TOKEN" \
  -d '{"emails":["alice@yourdomain.com"],"accessAll":false,"type":2}'

Restrict invitations to specific email domains:

environment:
  SIGNUPS_DOMAINS_WHITELIST: "yourdomain.com,yourcompany.com"
  SIGNUPS_ALLOWED: "false"    # Still disable open signups
  SIGNUPS_VERIFY: "true"      # Require email verification

Part 6: Organizations and Collections Deep Dive

Organization Roles

RoleCan Do
OwnerAll settings, billing, delete org
AdminManage users, collections
ManagerManage assigned collections
MemberAccess assigned collections
CustomGranular permissions

Collections Strategy

Design your collections for access control:

Organization: "Company"
├── Collection: "Shared Infrastructure" (Admins + DevOps team)
│   ├── AWS Root Account
│   ├── Kubernetes certs
│   └── VPN credentials
├── Collection: "Engineering" (All engineers - Read)
│   ├── GitHub tokens
│   └── Dev environment credentials
├── Collection: "Finance" (Finance team only)
│   ├── Bank accounts
│   └── Payment processors
└── Collection: "HR" (HR only)
    ├── HRIS credentials
    └── Benefits portal

Per-collection permissions:

  • Can view: See and copy passwords (can't see full password)
  • Can edit: Full CRUD, edit metadata
  • Hide passwords: Can autofill but never see the actual password

Part 7: Admin Audit Logs

Enable audit logging to track vault events:

environment:
  ORG_EVENTS_ENABLED: "true"      # Enable org event log
  EVENTS_DAYS_RETAIN: 90          # Keep events for 90 days

Events tracked:

  • Login attempts (success/failure)
  • Item created/modified/deleted
  • Collection access granted/revoked
  • 2FA enrolled/removed
  • Admin actions

View in Admin Panel → Event Logs or Organization → Event Log.


Part 8: Backup Strategy

PostgreSQL Backup

#!/bin/bash
# backup-vaultwarden.sh

set -e

DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/backup/vaultwarden"
mkdir -p "$BACKUP_DIR"

# Database dump:
docker exec vaultwarden_db pg_dump -U vaultwarden vaultwarden \
  | gzip > "$BACKUP_DIR/db-${DATE}.sql.gz"

# Attachments and icons:
tar -czf "$BACKUP_DIR/data-${DATE}.tar.gz" \
  $(docker volume inspect vaultwarden_vaultwarden_data --format '{{.Mountpoint}}')

# Remove backups older than 30 days:
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete

# Notify on success:
curl -s -d "Vaultwarden backup complete: db-${DATE}.sql.gz" \
  https://ntfy.yourdomain.com/backups

S3 Offsite Backup

# Add to backup script:
aws s3 sync "$BACKUP_DIR" "s3://your-bucket/vaultwarden/" \
  --exclude "*" --include "*.gz" \
  --delete

Part 9: Monitoring

Health Check

# Vaultwarden health endpoint:
curl https://vault.yourdomain.com/alive
# Returns: {"version":"...", "status":"ok"}

Uptime Kuma Monitor

  1. Add HTTP(S) monitor: https://vault.yourdomain.com/alive
  2. Alert via Slack/ntfy if it goes down

Prometheus Metrics

environment:
  # Metrics available at /metrics (enable in admin panel)
  METRICS_ENABLED: "true"
# prometheus.yml:
scrape_configs:
  - job_name: "vaultwarden"
    metrics_path: /metrics
    static_configs:
      - targets: ["vault.yourdomain.com:80"]
    basic_auth:
      username: metrics
      password: your-metrics-password

Part 10: Emergency Access Configuration

Set up emergency access before you need it:

  1. Settings → Emergency Access → Add Emergency Contact
  2. Enter their email (they need a Bitwarden account on your Vaultwarden)
  3. Type:
    • View: Can view all items in your vault
    • Takeover: Can reset your master password (full account access)
  4. Wait time: 5-7 days recommended
  5. They receive an email confirming their role

When they invoke emergency access:

  1. They click "Request access" in their Bitwarden app
  2. You receive an email and have the wait period to deny it
  3. After the wait period: access is automatically granted
  4. You can approve early or deny any time during the wait

Maintenance

# Update Vaultwarden:
docker compose pull
docker compose up -d

# Run migrations (usually auto-runs on startup):
docker compose logs vaultwarden | grep -i migrat

# Check connected clients:
# Admin → Users → [User] → Sessions

# Logs with timestamps:
docker compose logs -f --timestamps vaultwarden

# Admin panel: https://vault.yourdomain.com/admin

See all open source security and privacy tools at OSSAlt.com/categories/security.

Comments