Vaultwarden Advanced Setup: Security Hardening and Teams 2026
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):
- Users → select user → Enable 2FA Required
- 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
| Role | Can Do |
|---|---|
| Owner | All settings, billing, delete org |
| Admin | Manage users, collections |
| Manager | Manage assigned collections |
| Member | Access assigned collections |
| Custom | Granular 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
- Add HTTP(S) monitor:
https://vault.yourdomain.com/alive - 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:
- Settings → Emergency Access → Add Emergency Contact
- Enter their email (they need a Bitwarden account on your Vaultwarden)
- Type:
- View: Can view all items in your vault
- Takeover: Can reset your master password (full account access)
- Wait time: 5-7 days recommended
- They receive an email confirming their role
When they invoke emergency access:
- They click "Request access" in their Bitwarden app
- You receive an email and have the wait period to deny it
- After the wait period: access is automatically granted
- 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.