Vaultwarden vs Bitwarden vs 1Password (2026 Comparison)

Vaultwarden replaces Bitwarden's paid tiers for free. Here's the real feature comparison and our self-hosted setup guide.

Vaultwarden vs Bitwarden vs 1Password (2026 Comparison)
Photo by Vova Kondriianenko / Unsplash

LastPass was breached in 2022. Attackers got the encrypted password vaults of every customer. If you had a weak master password or reused it anywhere, your credentials were compromised. The breach was disclosed in December 2022, months after it happened.

1Password hasn't been breached — yet. But it's $36/year per person, $60/year for families. Every credential your team holds lives on infrastructure you don't own or control. Their business model requires them to hold your encrypted data on their servers. Their pricing requires them to keep you paying or you lose access to your passwords.

I'm not saying don't use a password manager. Not having one is far worse than having any of these. But the argument for self-hosting your password manager is the same argument for self-hosting anything: your data stays on hardware you control, the cost drops to electricity and storage after the initial setup, and you're not dependent on a vendor's continued existence or security practices.

Vaultwarden is the right answer for self-hosters. It's not "Bitwarden but cheaper" — it's a clean reimplementation of the Bitwarden server API in Rust that uses official Bitwarden clients. Every official Bitwarden app — browser extensions for Chrome, Firefox, Safari, Edge; mobile apps for iOS and Android; the desktop application; the CLI — connects to Vaultwarden as if it were the official Bitwarden server. Because it implements the same API, every client feature works: TOTP, Bitwarden Authenticator, passkeys, SSH keys, attachments, organizations, collections, emergency access. All of it.

The server itself runs in a single Docker container using around 10MB of RAM at idle. Our Vaultwarden instance at CoderOasis serves eight active users plus organization collections for shared credentials, and it uses less memory than a Chrome tab.

This is the dedicated deep dive that the self-hosted productivity stack guide summarized. We're going further: detailed SMTP configuration, the admin panel and everything it controls, YubiKey and TOTP 2FA setup, organization and collection management for teams, the backup strategy, emergency access configuration, and the hardening steps that matter for a service holding your most sensitive data.

What Vaultwarden Is and What It Isn't

Vaultwarden is an unofficial, community-developed reimplementation of the Bitwarden server API. It is not the official Bitwarden server (which is open-source but written in C# and designed for enterprise-scale multi-tenant hosting with separate services for each component). Vaultwarden is a single Rust binary that implements the same HTTP API, backed by SQLite or PostgreSQL.

What it is:

  • Complete compatibility with all official Bitwarden clients
  • Free to self-host with no per-user licensing
  • Extremely lightweight (10-50MB RAM under load)
  • Single binary / single container
  • Implements most Bitwarden features including organizations, TOTP, WebAuthn, passkeys, attachments, emergency access, Bitwarden Send
    What it isn't:
  • The official Bitwarden server
  • Officially supported by Bitwarden Inc.
  • A drop-in replacement for enterprise Bitwarden with Active Directory sync, SSO, advanced reporting
    License caveat: Vaultwarden is AGPL-3.0. If you're building a commercial service on top of it, read the license. For personal and team self-hosting, you're fine.

Architecture and Deployment

What You Need Before Starting

  • A server with Docker and Docker Compose installed
  • A domain name with DNS control (a subdomain like vault.yourdomain.com)
  • Traefik running as a reverse proxy (from the productivity stack guide) — or you can adapt the config to Nginx
  • An SMTP server or email service for account notifications
  • A valid TLS certificate (handled by Traefik + ACME in this guide)
    Vaultwarden requires HTTPS. The Bitwarden clients refuse to connect to any server without a valid TLS certificate. This is a non-negotiable security requirement and a good one.

Directory Setup

mkdir -p /opt/vaultwarden/data
# Data directory will contain: db.sqlite3, attachments/, sends/, icon_cache/, ...
# Never delete this directory

The Docker Compose Configuration

# /opt/vaultwarden/docker-compose.yml
version: "3.9"
 
networks:
  proxy:
    external: true    # Shared network with Traefik
 
services:
  vaultwarden:
    image: vaultwarden/server:latest   # Pin to a specific version in production
    # image: vaultwarden/server:1.31.0  # Pin version for reproducible deployments
    container_name: vaultwarden
    restart: unless-stopped
    networks:
      - proxy
    volumes:
      - ./data:/data    # All Vaultwarden state lives here
    environment:
      # ── REQUIRED ─────────────────────────────────────────────────────
      DOMAIN: "https://vault.yourdomain.com"
 
      # Admin token -- CHANGE THIS. Generate with:
      # openssl rand -base64 48
      # Then hash it: echo -n "your-token" | argon2 "$(openssl rand -base64 32)" -id -t 3 -m 64 -p 4 -l 32 -e
      # Or let Vaultwarden generate the argon2 hash: set the plain token here,
      # it will log the argon2 version on first start
      ADMIN_TOKEN: "$$argon2id$$v=19$$m=65540,t=3,p=4$$hashgoeshere"
 
      # ── REGISTRATION ─────────────────────────────────────────────────
      # DISABLE after you've created your accounts
      SIGNUPS_ALLOWED: "false"
 
      # Restrict registration to specific email domains (if SIGNUPS_ALLOWED is true)
      # SIGNUPS_DOMAINS_WHITELIST: "yourdomain.com,yourotherdomain.com"
 
      # Allow existing users to invite new users via email
      INVITATIONS_ALLOWED: "true"
 
      # ── EMAIL / SMTP ─────────────────────────────────────────────────
      SMTP_HOST: "mail.yourdomain.com"
      SMTP_FROM: "[email protected]"
      SMTP_FROM_NAME: "Vaultwarden"
      SMTP_SECURITY: "starttls"    # Options: off, starttls, force_tls
      SMTP_PORT: "587"
      SMTP_USERNAME: "[email protected]"
      SMTP_PASSWORD: "${SMTP_PASSWORD}"   # From .env file
 
      # Require email verification for new accounts
      REQUIRE_DEVICE_EMAIL: "true"
 
      # ── SECURITY ─────────────────────────────────────────────────────
      # Failed login count before lockout
      LOGIN_RATELIMIT_MAX_BURST: "10"
      LOGIN_RATELIMIT_SECONDS: "60"
 
      # Admin page rate limiting
      ADMIN_RATELIMIT_MAX_BURST: "3"
      ADMIN_RATELIMIT_SECONDS: "60"
 
      # Password hints -- disable for higher security
      PASSWORD_HINTS_ALLOWED: "false"
 
      # Show password hint on login page if user has set one
      SHOW_PASSWORD_HINT: "false"
 
      # ── WEBSOCKET ────────────────────────────────────────────────────
      # WebSocket for live sync -- clients get instant vault updates
      WEBSOCKET_ENABLED: "true"
      WEBSOCKET_ADDRESS: "0.0.0.0"
      WEBSOCKET_PORT: "3012"
 
      # ── PUSH NOTIFICATIONS ───────────────────────────────────────────
      # Enable push notifications for mobile apps (requires Bitwarden relay)
      # This sends vault change notifications to the Bitwarden push service
      # which then notifies your mobile app. The notification itself reveals
      # nothing sensitive -- just "there's been a change, please sync."
      # Optional: if you want instant mobile sync without opening the app
      # PUSH_ENABLED: "true"
      # PUSH_INSTALLATION_ID: "your-installation-id"
      # PUSH_INSTALLATION_KEY: "your-installation-key"
      # (Get these from https://bitwarden.com/host/ -- free registration)
 
      # ── STORAGE ──────────────────────────────────────────────────────
      # Maximum file attachment size (default 10MB)
      ATTACHMENTS_FOLDER: /data/attachments
 
      # Enable Bitwarden Send (encrypted file/text sharing)
      SENDS_ALLOWED: "true"
 
      # ── DATABASE ─────────────────────────────────────────────────────
      # Default: SQLite at /data/db.sqlite3
      # For PostgreSQL (better for larger teams):
      # DATABASE_URL: "postgresql://vaultwarden:password@postgres:5432/vaultwarden"
 
      # ── LOGGING ──────────────────────────────────────────────────────
      LOG_FILE: "/data/vaultwarden.log"
      LOG_LEVEL: "warn"    # Options: trace, debug, info, warn, error
      EXTENDED_LOGGING: "true"
 
      # Log failed logins (useful for monitoring and Fail2ban)
      IP_HEADER: "X-Real-IP"    # Get real client IP from Traefik header
 
    labels:
      # Traefik routing
      - "traefik.enable=true"
 
      # Main web UI
      - "traefik.http.routers.vaultwarden.rule=Host(`vault.yourdomain.com`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
 
      # Security headers -- required for password manager
      - "traefik.http.routers.vaultwarden.middlewares=vaultwarden-headers"
      - "traefik.http.middlewares.vaultwarden-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.vaultwarden-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.vaultwarden-headers.headers.stsPreload=true"
      - "traefik.http.middlewares.vaultwarden-headers.headers.browserXssFilter=true"
      - "traefik.http.middlewares.vaultwarden-headers.headers.contentTypeNosniff=true"
      - "traefik.http.middlewares.vaultwarden-headers.headers.referrerPolicy=same-origin"
 
      # WebSocket for live sync notifications
      - "traefik.http.routers.vaultwarden-ws.rule=Host(`vault.yourdomain.com`) && Path(`/notifications/hub`)"
      - "traefik.http.routers.vaultwarden-ws.entrypoints=websecure"
      - "traefik.http.routers.vaultwarden-ws.tls.certresolver=letsencrypt"
      - "traefik.http.services.vaultwarden-ws.loadbalancer.server.port=3012"
# /opt/vaultwarden/.env
# Never commit this file to git
SMTP_PASSWORD=your-smtp-password-here

Generating the Admin Token Properly

The admin token used to be stored as plaintext in older Vaultwarden versions. Current versions support Argon2 hashing for the token, which means the token hash is stored in the environment but the actual token is only in your head (or your personal password manager):

# Generate a random token
ADMIN_TOKEN=$(openssl rand -base64 48)
echo "Your admin token (save this somewhere safe): $ADMIN_TOKEN"
 
# Now generate the Argon2 hash that goes in docker-compose.yml
# Vaultwarden will accept either the plain token OR the hash
# Using the hash means the actual token isn't visible in your compose file
 
# If you have vaultwarden running, it can hash it for you:
docker run --rm -it vaultwarden/server:latest \
  /vaultwarden hash --preset owasp
# Enter your token at the prompt, copy the argon2id hash
 
# Or use the argon2 CLI tool:
# apt install argon2
# echo -n "$ADMIN_TOKEN" | argon2 "$(openssl rand -base64 32)" -id -t 3 -m 16 -p 4 -l 32 -e

In the compose file, set ADMIN_TOKEN to the hash. In your personal password manager, save the unhashed token. You'll use the unhashed token when accessing the admin panel.

Starting Vaultwarden

cd /opt/vaultwarden
docker compose up -d
 
# Watch startup logs
docker logs vaultwarden -f
 
# Healthy startup looks like:
# [INFO] Starting web server on 0.0.0.0:80
# [INFO] Starting WebSockets server on 0.0.0.0:3012

Navigate to https://vault.yourdomain.com. You should see the Bitwarden login page with a valid TLS certificate. If you see a certificate warning, Traefik hasn't issued the Let's Encrypt certificate yet — wait 60 seconds and refresh.

First-Time Setup: Creating Accounts

Temporarily Enable Registration

With SIGNUPS_ALLOWED: "false" (as set in the config), registration is disabled. You need to temporarily enable it to create your accounts, then disable it again.

Two options:

Option 1: Via admin panel invite

  1. Navigate to https://vault.yourdomain.com/admin — log in with your admin token
  2. Go to Users tab → Invite User
  3. Enter the email address, send the invite
  4. The user receives an email and completes registration via invite link
  5. Registration is closed to anyone without an invite
    Option 2: Temporarily enable and disable
# Temporarily enable signups
docker compose exec vaultwarden sh -c \
  "SIGNUPS_ALLOWED=true /vaultwarden"
 
# Better: update the env var, restart, create accounts, re-disable, restart

The invite approach is cleaner. Use the admin panel's invite function rather than ever enabling open registration.

Connecting the Bitwarden Client

After account creation, point any Bitwarden client to your self-hosted instance:

Browser Extension (Chrome/Firefox/Edge/Safari):

  1. Install the Bitwarden extension from your browser's extension store
  2. Click the extension icon → click the region selector dropdown (defaults to "US")
  3. Select "Self-hosted"
  4. Enter https://vault.yourdomain.com as the Server URL
  5. Click Save, then log in with your credentials
    Mobile App (iOS/Android):
  6. Install the official Bitwarden app
  7. On the login screen, tap the region selector
  8. Select "Self-hosted"
  9. Enter your server URL
  10. Log in
    Desktop App:
    Same process as mobile — settings → change server URL → enter your vault URL.

CLI:

# Install bw CLI
npm install -g @bitwarden/cli
 
# Configure to use your server
bw config server https://vault.yourdomain.com
 
# Log in
bw login
 
# Unlock the vault
bw unlock
 
# Use normally
bw list items
bw get password "GitHub"

The Admin Panel: Everything It Controls

The admin panel at /admin is the operational center of your Vaultwarden instance. Access it with your admin token (not your vault login). Here's what each section does and the settings that matter:

General Settings

Settings → General Settings
 
# These should match your docker-compose env vars
# Changes here take effect immediately without restart

Domain URL: Must match exactly what's in DOMAIN. Mismatches cause CORS errors in clients.

Allow signups: Should be OFF. If you need to add a user, use the invite function.

Require email verification: Should be ON. Prevents account creation with fake email addresses.

Disable admin token: Do not check this. It permanently disables the admin panel with no way to re-enable it without modifying the database directly.

SMTP Settings via Admin Panel

You can configure SMTP from the admin panel instead of environment variables. The advantage: changes apply without container restart. Navigate to Settings → SMTP Email Settings:

Host: mail.yourdomain.com
Security method: STARTTLS
SMTP Port: 587
From address: [email protected]
From name: Vaultwarden
Username: [email protected]
Password: [your SMTP password]

After saving, use the Send test email button to verify delivery. Check spam folders if the test doesn't arrive — many email providers filter server-sent mail aggressively.

If you're self-hosting your email with Mailcow (see the Mailcow guide), create a dedicated [email protected] mailbox and use its SMTP credentials here.

User Management

Admin panel → Users shows all registered accounts with their email, last login time, 2FA status, and whether they're confirmed.

Critical actions here:

  • Deactivate: Immediately prevents a user from logging in. Use this for offboarding. Their vault data is preserved.
  • Delete: Permanently removes the user and their private vault data. Cannot be undone.
  • Resend email confirmation: If a user's verification email expired.
    The 2FA column shows whether each user has MFA configured. For a team deployment, you should require 2FA for all users. You can do this by policy (tell everyone to set it up) or through a Vaultwarden admin policy — see the Organizations section.

Diagnostics

Admin panel → Diagnostics runs self-tests on your instance:

  • Database connectivity
  • SMTP connectivity and delivery test
  • System information (disk space, memory)
  • Binary version and compilation info
    Run this after any configuration change to verify everything is working. The disk space check is important — Vaultwarden's attachments folder grows over time, and running out of space causes silent write failures.

Two-Factor Authentication

Why 2FA on Your Password Manager Is Non-Negotiable

Your Vaultwarden master password is the single key to every other credential you own. If an attacker phishes your master password, or if you use it elsewhere and it's breached, everything else falls. 2FA means the master password alone isn't enough.

Vaultwarden supports:

  • TOTP (Time-based One-Time Passwords): Google Authenticator, Authy, Bitwarden Authenticator, any TOTP app
  • WebAuthn/FIDO2: Hardware security keys (YubiKey 5, Google Titan), Touch ID, Face ID, Windows Hello
  • Email OTP: One-time codes sent to email (weakest option, avoid if you can)
  • Duo: Enterprise 2FA service
  • Bitwarden Authenticator: Bitwarden's built-in TOTP app on mobile

YubiKeys are the gold standard for 2FA. We use them at CoderOasis for every privileged account including Vaultwarden. The FIDO2/WebAuthn implementation is phishing-resistant — the key cryptographically verifies the domain, so a fake site can't intercept your authentication.

In the Bitwarden client:

  1. Open Settings → Security → Two-Step Login
  2. Click Manage next to "FIDO2 WebAuthn"
  3. Click Add FIDO2 WebAuthn Credential
  4. Enter your master password when prompted
  5. Insert your YubiKey and tap it when prompted
  6. Save — the key is now enrolled
    Add at least two keys: your primary YubiKey and a backup. Store the backup in a physically secure location. If you lose your only hardware key and you're locked out of your vault, recovery depends on whether you set up an emergency contact (see below) or if you can access the admin panel.

From the admin panel you can also remove 2FA from a user account if they're locked out — navigate to Users → find the user → Remove 2FA. Only use this as a last resort; it creates a window of reduced security.

Setting Up TOTP (Backup Method)

Even with a hardware key, set up TOTP as a backup method. If your key is temporarily unavailable, TOTP is your fallback.

  1. Open Settings → Security → Two-Step Login
  2. Click Manage next to "Authenticator App"
  3. Open your TOTP app, scan the QR code
  4. Enter the generated code to verify
  5. Save your recovery codes — these are one-time-use codes that bypass 2FA
    Store recovery codes in a physically secure location — printed and in a safe, not in a digital note. If you lose both your hardware key and your TOTP app, recovery codes are your last resort before needing admin intervention.

Organizations and Collections: Managing Team Access

For a team (CoderOasis's setup), you want an Organization with Collections. This lets you share specific credentials with specific groups without sharing everything.

Creating an Organization

  1. In the Bitwarden web vault (https://vault.yourdomain.com), click New Organization
  2. Enter organization name, billing email (ignored for self-hosted)
  3. Choose Free plan (all features are free on self-hosted Vaultwarden)
    Organizations in Vaultwarden support unlimited users, unlimited collections, and all organizational features including:
  • Member roles (Owner, Admin, Manager, Member, Custom)
  • Collections (like folders shared across members)
  • Groups (members organized into groups, collections assigned to groups)
  • Policies (require 2FA, master password requirements, etc.)

Collection Structure

Think of Collections as shared folders. Design them around access boundaries:

CoderOasis (Organization)
├── Infrastructure          # Servers, Traefik, DNS -- platform team only
├── Social Media            # Twitter, LinkedIn, Mastodon -- editorial team
├── Analytics               # Plausible, GA, etc -- all team members
├── Code Repositories       # GitHub tokens, GitLab -- engineers
├── Email Services          # Mailcow admin -- platform team only
├── Shared Services         # Tools everyone uses (Figma, Notion, etc)
└── Emergency               # Break-glass credentials -- owners only

Member Roles

Role What They Can Do
Owner Full control including billing settings and destructive actions
Admin Can manage members and collections, cannot delete the organization
Manager Can manage collections assigned to them
Member Can use credentials in assigned collections
Custom Granular permissions -- useful for read-only access

For CoderOasis: Traven and one trusted admin are Owners. Core team members are Members with access to the collections they need. Guest contributors get Member access only to the Social Media and Shared Services collections.

Organizational Policies (Vaultwarden)

Policies let you enforce security requirements across your organization. Navigate to Organization Settings → Policies:

Require Two-Step Login: Prevents members from accessing organization vaults without 2FA enabled. Recommended for any team deployment.

Master Password Requirements: Set minimum length and complexity for master passwords. Set minimum to 16 characters.

Single Organization: Prevents members from joining other Bitwarden organizations from the same account. For sensitive environments where credential cross-contamination is a concern.

Disable Personal Vault Export: Prevents members from exporting their personal vault. Consider this for high-security environments.

Remove Individual Vault: Forces all items to be in organizational collections rather than personal vaults. Ensures no sensitive team credentials sit in personal vaults that don't get backed up or managed organizationally.

PostgreSQL Backend (For Larger Teams)

SQLite works fine for small teams (under ~20 users with moderate usage). For larger teams or if you want better concurrent access and easier backup integration, switch to PostgreSQL.

Add a PostgreSQL container:

# Add to docker-compose.yml
  postgres:
    image: postgres:16-alpine
    container_name: vaultwarden-postgres
    restart: unless-stopped
    networks:
      - proxy
    environment:
      POSTGRES_DB: vaultwarden
      POSTGRES_USER: vaultwarden
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    # No port exposure -- internal only
 
volumes:
  postgres_data:

Update the vaultwarden service to use PostgreSQL:

    environment:
      DATABASE_URL: "postgresql://vaultwarden:${POSTGRES_PASSWORD}@postgres:5432/vaultwarden"
      # Remove the DATABASE_URL for SQLite (it'll default to SQLite without this)

Migrate from SQLite to PostgreSQL:

# Stop Vaultwarden
docker compose stop vaultwarden
 
# The migration isn't automatic -- Vaultwarden doesn't provide a SQLite→Postgres migrator
# Options:
# 1. Start fresh with Postgres (for new deployments)
# 2. Use the community tool: https://github.com/GhostWriters/sqlite_to_postgresql
# 3. Use bw CLI export/import (exports JSON, re-imports into new instance)
 
# For an existing deployment, option 3 is the safest:
# 1. Export vault via bw CLI or web UI (Actions → Export → JSON)
# 2. Stand up fresh Vaultwarden with PostgreSQL
# 3. Recreate accounts and import vault JSON
# 4. Re-invite all users (invites from old instance don't transfer)

Backup Strategy: The One That Won't Let You Down

Vaultwarden's data lives in /opt/vaultwarden/data/. Everything is there:

/opt/vaultwarden/data/
├── db.sqlite3              # Main SQLite database (all vaults, orgs, settings)
├── db.sqlite3-wal          # SQLite write-ahead log (MUST back up with db.sqlite3)
├── db.sqlite3-shm          # SQLite shared memory file
├── attachments/            # File attachments uploaded to vault items
├── sends/                  # Bitwarden Send files
└── icon_cache/             # Favicon cache (not critical, will rebuild)

The -wal file is critical. SQLite uses write-ahead logging, and the WAL file contains recent writes that haven't been checkpointed into the main database file. Backing up db.sqlite3 without db.sqlite3-wal gives you a potentially incomplete backup. Always back up both together, or use SQLite's backup API to get a consistent snapshot.

Automated Backup Script

#!/bin/bash
# /opt/scripts/vaultwarden-backup.sh
# Run daily via cron
 
set -euo pipefail
 
BACKUP_DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR="/opt/backups/vaultwarden"
VAULTWARDEN_DATA="/opt/vaultwarden/data"
REMOTE="user@backup-server:/backups/vaultwarden"
 
mkdir -p "${BACKUP_DIR}"
 
echo "[${BACKUP_DATE}] Starting Vaultwarden backup..."
 
# Use SQLite's .backup command for a consistent snapshot
# This is safer than copying the files directly while Vaultwarden is running
# because SQLite's backup API checkpoints the WAL automatically
 
docker exec vaultwarden sqlite3 /data/db.sqlite3 \
  ".backup /data/db-backup-${BACKUP_DATE}.sqlite3"
 
# Copy the backup and attachments
mkdir -p "${BACKUP_DIR}/${BACKUP_DATE}"
docker cp "vaultwarden:/data/db-backup-${BACKUP_DATE}.sqlite3" \
  "${BACKUP_DIR}/${BACKUP_DATE}/db.sqlite3"
docker cp "vaultwarden:/data/attachments" \
  "${BACKUP_DIR}/${BACKUP_DATE}/" 2>/dev/null || true
docker cp "vaultwarden:/data/sends" \
  "${BACKUP_DIR}/${BACKUP_DATE}/" 2>/dev/null || true
 
# Clean up the in-container backup file
docker exec vaultwarden rm "/data/db-backup-${BACKUP_DATE}.sqlite3"
 
# Compress the backup
tar -czf "${BACKUP_DIR}/${BACKUP_DATE}.tar.gz" \
  -C "${BACKUP_DIR}" "${BACKUP_DATE}/"
rm -rf "${BACKUP_DIR}/${BACKUP_DATE}/"
 
echo "  ✓ Local backup: ${BACKUP_DIR}/${BACKUP_DATE}.tar.gz"
 
# Encrypt before sending offsite (using age encryption)
# age keygen -o /opt/backup-key.txt (run once, store backup-key.txt safely)
# age -r $(grep '^public key:' /opt/backup-key.txt | awk '{print $NF}') \
#   -o "${BACKUP_DIR}/${BACKUP_DATE}.tar.gz.age" \
#   "${BACKUP_DIR}/${BACKUP_DATE}.tar.gz"
 
# Sync to remote backup location
rsync -avz "${BACKUP_DIR}/${BACKUP_DATE}.tar.gz" "${REMOTE}/"
 
# Also sync to a second location (cloud storage via rclone)
# rclone copy "${BACKUP_DIR}/${BACKUP_DATE}.tar.gz" "b2:vault-backups/"
 
echo "  ✓ Remote sync complete"
 
# Retain last 30 days of local backups
find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +30 -delete
 
echo "[${BACKUP_DATE}] Backup complete."
# Set up daily at 2 AM
echo "0 2 * * * root /opt/scripts/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1" \
  >> /etc/crontab

Testing Your Backup

Run this monthly. Don't assume backups work until you've tested restoration:

# Restore test procedure -- do this in a separate test environment
 
# 1. Extract backup
tar -xzf /opt/backups/vaultwarden/20260501-0200.tar.gz -C /tmp/vw-restore-test/
 
# 2. Stand up a test Vaultwarden instance pointing at the backup database
mkdir /tmp/vw-test
cp /tmp/vw-restore-test/db.sqlite3 /tmp/vw-test/
cp -r /tmp/vw-restore-test/attachments/ /tmp/vw-test/ 2>/dev/null || true
 
docker run --rm -d \
  -p 8080:80 \
  -v /tmp/vw-test:/data \
  -e DOMAIN=http://localhost:8080 \
  -e ADMIN_TOKEN=test-admin-token \
  -e SIGNUPS_ALLOWED=false \
  --name vw-test \
  vaultwarden/server:latest
 
# 3. Connect a Bitwarden client to http://localhost:8080
# Log in with a real account from the backup
# Verify credentials are intact, attachments are accessible
# Verify organization collections are present
 
# 4. Clean up
docker stop vw-test
rm -rf /tmp/vw-test /tmp/vw-restore-test

If the test succeeds, you're confident in your backup. If it fails, investigate immediately — don't wait until you need the backup in production.

Fail2ban Integration

Vaultwarden logs failed authentication attempts. Fail2ban can parse those logs and block IPs after repeated failures. This protects against brute force attacks against user accounts.

apt install -y fail2ban
# /etc/fail2ban/filter.d/vaultwarden.conf
[INCLUDES]
before = common.conf
 
[Definition]
# Match lines like:
# [2026-05-01 03:42:15][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.42.
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>.*$
 
# Also catch admin panel failures
# [2026-05-01 03:42:16][vaultwarden::api::admin][ERROR] Invalid admin token. IP: 203.0.113.42
failregex = ^.*Invalid admin token\. IP: <ADDR>.*$
 
ignoreregex =
# /etc/fail2ban/jail.d/vaultwarden.conf
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 3600      # 1 hour ban
findtime = 300      # Within 5 minutes
 
[vaultwarden-admin]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/vaultwarden/data/vaultwarden.log
maxretry = 3
bantime = 86400     # 24 hour ban for admin panel failures
findtime = 300
systemctl restart fail2ban
 
# Verify the jail is active
fail2ban-client status vaultwarden
fail2ban-client status vaultwarden-admin
 
# Test: check banned IPs
fail2ban-client status vaultwarden | grep "Banned IP"

Emergency Access

Bitwarden's Emergency Access feature lets you designate a trusted contact who can request access to your vault if you're incapacitated or deceased. After a waiting period you set (1-90 days), the contact gets read or full access to your vault items.

Configure it under Settings → Emergency Access → Add Emergency Contact:

  1. Enter the email of your trusted contact (they must have a Bitwarden/Vaultwarden account)
  2. Choose access type: View (read-only) or Takeover (can change master password and take full control)
  3. Set the waiting period (we recommend 7-14 days — enough time for you to reject a fraudulent request if you're alive, short enough to be useful)
  4. Your contact receives an invite to confirm they're willing to be your emergency contact
    When a contact requests emergency access, you receive an email notification. You have the waiting period to reject the request. If you don't respond (because you can't), access is granted automatically after the waiting period.

This is one of the features that makes Bitwarden/Vaultwarden superior to many alternatives for personal use. It solves the "I have all the credentials, what happens to my family if I die" problem that every security-conscious person should think about.

Updating Vaultwarden

Vaultwarden releases frequently. Database migrations run automatically on startup. The update process is:

# Pull the new image
docker compose pull
 
# Recreate the container (no data loss -- data is in the volume)
docker compose up -d --force-recreate vaultwarden
 
# Verify it started successfully
docker logs vaultwarden --since=5m
 
# Check the version at /admin → Diagnostics

Before any update: Make a fresh backup. The database schema migrations are generally reliable but a pre-update backup takes 30 seconds and gives you a rollback point.

Rolling back if an update breaks something:

# Stop the broken version
docker compose stop vaultwarden
 
# Restore from pre-update backup
# (Follow the backup restoration procedure from earlier)
 
# Run the old image version explicitly
docker run -d \
  --name vaultwarden \
  -v /opt/vaultwarden/data:/data \
  -e ... \
  vaultwarden/server:1.30.5   # The previous working version

Monitoring Vaultwarden

Vaultwarden doesn't have a native Prometheus metrics endpoint. But you can monitor everything that matters from outside:

From Prometheus Blackbox Exporter:

  • HTTP probe https://vault.yourdomain.com — is the web UI responding?
  • SSL certificate expiry monitoring — critical for a TLS-required service
    From log monitoring (Grafana Loki or similar):
# Ship Vaultwarden logs to Loki
# In Grafana, create alerts on:
{container="vaultwarden"} |= "Username or password is incorrect"
{container="vaultwarden"} |= "Invalid admin token"
{container="vaultwarden"} |= "ERROR"

Alert when Vaultwarden is down:

Add to your Prometheus blackbox-http targets:

- targets:
    - https://vault.yourdomain.com

The existing EndpointDown and SSLCertExpiryCritical alert rules from the Prometheus guide will fire automatically.

Security Hardening Checklist

Run through this after initial setup and after any significant configuration change:

# 1. Verify signups are disabled
curl -s https://vault.yourdomain.com/api/accounts/register \
  -X POST -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","masterPasswordHash":"test","kdfType":0,"kdfIterations":600000}' | \
  grep -q "Registration not allowed" && echo "✓ Signups disabled" || echo "⚠ Signups may be enabled"
 
# 2. Verify admin panel requires authentication
curl -s -o /dev/null -w "%{http_code}" https://vault.yourdomain.com/admin
# Should return 200 (login page), not 302 to admin content
 
# 3. Verify TLS configuration
curl -I https://vault.yourdomain.com | grep -E "Strict-Transport-Security|X-Content-Type"
 
# 4. Verify WebSocket is working (client real-time sync)
# Check browser developer tools when logged in: should see active WSS connection
 
# 5. Check logs for unexpected admin access
grep "Admin authenticated" /opt/vaultwarden/data/vaultwarden.log | tail -20
 
# 6. Verify Fail2ban is running
fail2ban-client status vaultwarden
 
# 7. Test backup integrity
# Run the backup restoration test procedure from the backup section
 
# 8. Verify all users have 2FA enabled
# Admin panel → Users → check 2FA column for every user

Migrating From LastPass, 1Password, or Bitwarden Cloud

From LastPass

# LastPass export:
# LastPass website → Vault → More Options → Export
# Saves as lastpass_export.csv
 
# Install bw CLI
npm install -g @bitwarden/cli
 
# Configure to your server
bw config server https://vault.yourdomain.com
 
# Import LastPass CSV
bw login
bw import lastpasscsv lastpass_export.csv
 
# Verify import succeeded
bw list items | jq '.[].name' | head -20

From 1Password

# 1Password export:
# 1Password app → File → Export → 1Password Interchange Format (.1pif) or CSV
 
# Import via bw CLI
bw import onepassword1pif 1password_export.1pif
 
# Or CSV format
bw import onepasswordcsv 1password_export.csv

From Bitwarden Cloud

This is the cleanest migration since you're staying on the same protocol:

# Export from Bitwarden Cloud (web vault):
# Tools → Export Vault → JSON format (encrypted or unencrypted)
 
# Import to your self-hosted instance:
bw config server https://vault.yourdomain.com
bw login
bw import bitwardenjson bitwarden_export.json

Important: Exports don't include:

  • Attachments (download and re-upload manually)
  • Organization items (export and import organizations separately)
  • Passkeys (must re-register on the new instance)
  • 2FA configurations (must re-set up on new accounts)
    After migration, don't delete your LastPass/1Password account immediately. Run both for 30 days, verify everything is in Vaultwarden, then cancel.

Frequently Asked Questions

Is Vaultwarden as secure as Bitwarden Cloud?

The vault encryption is identical — client-side AES-256 encryption before anything is sent to the server. Bitwarden (and Vaultwarden) never see your plaintext credentials. The security difference is operational: Bitwarden Cloud has professional security operations, regular audits, and incident response teams. Your self-hosted instance has you. If you maintain it well (updates, backups, monitoring, fail2ban, proper TLS), it's comparable. If you let it sit unpatched for months, it's not.

What happens to my passwords if Vaultwarden goes down?

Bitwarden clients cache your vault locally. If the server is unreachable, you can still access and auto-fill all your existing credentials. You just can't sync new items or access the web vault. This is one of the significant advantages over a pure web-based solution.

Can I use passkeys with Vaultwarden?

Yes. Bitwarden added passkey (FIDO2 credential) storage support in 2023, and Vaultwarden supports it. You can store passkeys in your vault and use them across devices via the Bitwarden client. Note: this is different from using a hardware key for 2FA on Vaultwarden itself.

How many users can a Vaultwarden instance support?

Comfortably: dozens to low hundreds with SQLite. Organizations with hundreds of users should use the PostgreSQL backend. Vaultwarden's resource consumption is dominated by the SQLite WAL and memory, not by user count — the encrypted vault data sits in the database, and most operations are reads.


Vaultwarden is one of the few self-hosted services I'd call genuinely essential infrastructure rather than a nice-to-have. Credentials are the attack surface for everything else. Centralizing them in a service you control, with proper 2FA, a working backup strategy, and monitoring means your credential hygiene is as strong as your operational discipline — and you're not at the mercy of a vendor's security practices or pricing changes. For the broader self-hosted stack context, check the self-hosted productivity stack article and the Teleport zero-trust access guide — Vaultwarden behind Teleport's application proxy is how we give team members access to the admin panel without exposing it to the internet.