The Trust Dependency

Every credential your organisation holds — email accounts, cloud consoles, payment gateways, internal tools, production databases, CI/CD pipelines — lives somewhere. For most organisations, that somewhere is a third-party password manager's cloud infrastructure. You trust the provider's security team, their patching cadence, their employee vetting, their access controls, their incident response, their jurisdiction's legal framework, and their business continuity planning. That is a substantial chain of trust for the keys to your entire operation.

LastPass demonstrated what happens when that trust is misplaced. Encrypted vaults were exfiltrated. Master password hashes were exposed to offline brute-force attacks. Users with weak master passwords — and there were many — had their vaults cracked. The breach was not a sophisticated zero-day chain — it was a failure of basic operational security on the provider's side. A developer's home machine was compromised. That machine had access to production infrastructure. The rest followed predictably.

The Bitwarden cloud is well-run. Their security model is sound — zero-knowledge architecture, client-side encryption, regular third-party audits. But it is still a trust dependency — your credentials sit on infrastructure you do not control, under a jurisdiction you did not choose, subject to legal processes you may not be notified about, operated by employees you have not vetted. For organisations that need to eliminate that dependency — regulated industries, government contractors, security-conscious teams, or anyone who has watched enough breaches to question the model — self-hosting is the answer.

What Vaultwarden Is

Vaultwarden is a community-written Rust implementation of the Bitwarden server API. It is not an official Bitwarden product — it was formerly called bitwarden_rs before being renamed to avoid trademark confusion.

The official Bitwarden server is a .NET application stack consisting of multiple services, requiring Microsoft SQL Server for its database backend. It is designed for enterprise-scale deployments and carries corresponding resource requirements — multiple containers, significant memory consumption, and operational complexity that is overkill for small teams and individual organisations.

Vaultwarden takes a different approach. It is a single Rust binary, compiled into a single Docker container, using SQLite by default for its database. It implements the full Bitwarden API — every official Bitwarden client works with it unmodified. Browser extensions, mobile apps, desktop applications, the CLI tool — all function identically whether they are pointed at Bitwarden's cloud, the official self-hosted server, or Vaultwarden.

The trade-off is explicit. You lose Bitwarden's managed infrastructure, their dedicated security team, their uptime SLA, and their support channel. You gain full control over your data, your encryption keys, your backup strategy, your network placement, your access logs, and your compliance posture. For organisations that can operate infrastructure — and if you are reading this, you likely can — the trade favours control.

Installation via Docker

Start with a clean Ubuntu 22.04 or 24.04 server. A virtual machine works. A small cloud instance works. A spare machine under a desk works, provided it has reliable power and network. The resource requirements are modest — Vaultwarden runs comfortably on 1 GB of RAM.

Install Docker and Docker Compose if they are not already present:

sudo apt update && sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable --now docker

Create a directory for the Vaultwarden configuration and data:

sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden

Create the Docker Compose file at /opt/vaultwarden/docker-compose.yml:

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - ./vw-data:/data
    environment:
      DOMAIN: "https://vault.example.com"
      SIGNUPS_ALLOWED: "true"
      ADMIN_TOKEN: "your-very-long-random-admin-token"
      LOG_FILE: "/data/vaultwarden.log"
      SMTP_HOST: "smtp.example.com"
      SMTP_FROM: "vault@example.com"
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "vault@example.com"
      SMTP_PASSWORD: "smtp-password-here"
    ports:
      - "127.0.0.1:8080:80"

The key environment variables:

The ports directive binds only to localhost — 127.0.0.1:8080:80. Vaultwarden is not directly exposed to the network. All external access goes through the reverse proxy.

Start the container:

sudo docker compose up -d

Vaultwarden is now running on localhost port 8080. It serves HTTP only — TLS termination is the reverse proxy's responsibility.

Apache Reverse Proxy with TLS

Install Apache and certbot:

sudo apt install -y apache2 certbot python3-certbot-apache
sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers rewrite

Enable the required Apache modules. The proxy_wstunnel module is critical — it handles WebSocket connections, which Vaultwarden uses for real-time synchronisation between clients.

Point your DNS at the server. The A record for vault.example.com must resolve to this machine's public IP before certbot can issue a certificate.

Obtain a Let's Encrypt certificate:

sudo certbot --apache -d vault.example.com

Create the Apache virtual host configuration at /etc/apache2/sites-available/vaultwarden.conf:

<VirtualHost *:443>
    ServerName vault.example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/vault.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/vault.example.com/privkey.pem

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass /notifications/hub ws://127.0.0.1:8080/notifications/hub
    ProxyPassReverse /notifications/hub ws://127.0.0.1:8080/notifications/hub

    ProxyPass / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/

    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"

    Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>

<VirtualHost *:80>
    ServerName vault.example.com
    RewriteEngine On
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>

Enable the site and restart Apache:

sudo a2ensite vaultwarden.conf
sudo systemctl restart apache2

The /notifications/hub WebSocket endpoint enables live sync — when you save a credential on one device, it appears on all others within seconds. Without the WebSocket proxy configured correctly, clients fall back to periodic polling. This works, but synchronisation becomes noticeably slower and generates unnecessary HTTP traffic.

The HSTS header tells browsers to always use HTTPS for this domain. Once set, browsers will refuse to connect over plain HTTP — even if the user types http:// explicitly. The max-age of two years is appropriate for a service you intend to keep running.

Verify the setup by navigating to https://vault.example.com in a browser. You should see the Bitwarden web vault interface. Create your first account.

Database Selection

SQLite is the default database engine and works adequately for small deployments — a household, a small team of fewer than a dozen users. The entire database is a single file at /opt/vaultwarden/vw-data/db.sqlite3. Backups are simple — copy the file. Recovery is simple — restore the file.

The limitation is concurrency. SQLite uses file-level locking for writes. A backup operation running at the same time as a write can produce a corrupted backup file — or worse, a backup that appears valid but contains inconsistent data. For a single user checking their vault, this is unlikely to be a problem. For an organisation with dozens of users across time zones, it becomes a real risk.

For organisations with more than a handful of users, or where backup reliability is a strict requirement, switch to MySQL or MariaDB.

Install MariaDB:

sudo apt install -y mariadb-server
sudo mysql_secure_installation

Answer yes to the security prompts — set a root password, remove anonymous users, disable remote root login, remove the test database.

Create the Vaultwarden database and user:

sudo mysql -u root -p
CREATE DATABASE vaultwarden CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'vaultwarden'@'localhost' IDENTIFIED BY 'strong-database-password';
GRANT ALL PRIVILEGES ON vaultwarden.* TO 'vaultwarden'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Add the database URL to your Docker Compose environment variables:

DATABASE_URL: "mysql://vaultwarden:strong-database-password@host.docker.internal/vaultwarden"

Restart the container. Vaultwarden detects the MySQL backend and creates its schema on first connection. If you are migrating from an existing SQLite deployment, the cleanest path is to export each user's vault as an encrypted JSON file from the web interface, switch the database backend, recreate the accounts, and reimport. The Vaultwarden documentation describes alternative migration methods, but the export-reimport approach is the least error-prone.

MariaDB also simplifies backup verification. You can restore a mysqldump to a test instance and confirm the data is intact — something that is harder to validate with a copied SQLite file, where corruption may not be apparent until a specific query hits the damaged page.

Hardening

Once your initial accounts are created and verified, lock down the deployment. Every hardening step reduces the attack surface available to anyone who discovers your Vaultwarden instance.

Disable public registration. Change SIGNUPS_ALLOWED to false in the Docker Compose file and restart the container. New accounts can still be created through the admin panel or by sending email invitation links — both of which require administrator action. No one can self-register.

sudo docker compose down

Edit the compose file, change SIGNUPS_ALLOWED to "false", then bring it back up:

sudo docker compose up -d

Restrict the admin panel. The ADMIN_TOKEN grants full control over the Vaultwarden instance — user management, organisation management, server diagnostics. After initial setup, consider removing the ADMIN_TOKEN variable from the environment entirely. This disables the admin panel completely. When you need it again — to invite a new user, to check diagnostics — add the token back temporarily, restart the container, do what you need, remove it, restart again. An always-available admin panel is an always-available attack surface.

Fail2Ban integration. Vaultwarden logs failed authentication attempts to the log file specified by LOG_FILE. Write a Fail2Ban filter that matches these failures and a jail that bans the source IP after repeated attempts.

Create the filter at /etc/fail2ban/filter.d/vaultwarden.conf:

[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

Create the jail at /etc/fail2ban/jail.d/vaultwarden.local:

[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/vaultwarden/vw-data/vaultwarden.log
maxretry = 5
bantime = 900
findtime = 600

Restart Fail2Ban. Five failed login attempts within ten minutes triggers a fifteen-minute ban on the source IP. Adjust the thresholds to your tolerance. The Fail2Ban configuration approach is covered in more detail in the Fail2Ban tutorial.

UFW firewall rules. Allow only port 443 inbound. SSH access on port 22 should be restricted to your administrative IP. Block everything else. The UFW tutorial covers the full syntax.

sudo ufw allow 443/tcp
sudo ufw allow from YOUR_ADMIN_IP to any port 22 proto tcp
sudo ufw enable

Rate limiting. Vaultwarden supports application-level rate limiting via the LOGIN_RATELIMIT_SECONDS and LOGIN_RATELIMIT_MAX_BURST environment variables. These limit the rate of login attempts per IP address at the application layer — a second line of defence behind Fail2Ban. Set LOGIN_RATELIMIT_SECONDS to 60 and LOGIN_RATELIMIT_MAX_BURST to 10 as a starting point.

HTTPS only. Ensure the ROCKET_TLS environment variable is not set — TLS termination is handled by Apache, not by Vaultwarden. But verify that the DOMAIN variable uses https://. Vaultwarden uses this value to construct URLs in emails and API responses. If it says http://, clients may attempt unencrypted connections.

Disable unnecessary features. If your organisation does not use the Bitwarden Send feature — temporary encrypted file and text sharing — disable it with SENDS_ALLOWED=false. If you do not need organisation support, you can leave it enabled but monitor for unexpected organisation creation through the admin panel. Each enabled feature is additional attack surface.

Container isolation. Run Docker with the default bridge network. Do not use --network=host. The container should have no access to the host network beyond the published port. If you are running multiple containers on the same host — a reverse proxy, a database, Vaultwarden — use a dedicated Docker network for communication between them and avoid exposing unnecessary ports.

Backup Strategy

Your password vault is, by definition, the keys to everything else. Losing it is not an inconvenience — it is an operational catastrophe. Every credential, every secure note, every TOTP seed, every shared organisation secret — gone. Treat backups with corresponding gravity.

For MariaDB, a daily mysqldump captures the current state:

mysqldump -u vaultwarden -p vaultwarden | gzip > /backup/vaultwarden-db-$(date +%Y%m%d).sql.gz

For the data directory — which contains file attachments, cached icons, the RSA keys used for JWT token signing, and the application log:

tar czf /backup/vaultwarden-data-$(date +%Y%m%d).tar.gz /opt/vaultwarden/vw-data

Encrypt the backups before storing them offsite. Your password vault backup should not be readable by anyone who obtains the backup media. GPG symmetric encryption works:

gpg --symmetric --cipher-algo AES256 /backup/vaultwarden-db-$(date +%Y%m%d).sql.gz

Store encrypted backups in a physically and logically separate location — a different server, a different cloud provider, a different physical site. The backup of your password manager should not be stored behind a password that is in the password manager. That is a circular dependency that fails precisely when you need the backup most.

Automate the process. A cron job running the dump, compress, encrypt, and transfer steps nightly is the minimum. Test the restore process quarterly — a backup you have never tested is a hypothesis, not a control.

The bus factor is the question that keeps operations honest. If you are the only person who knows the admin token, the database password, the backup encryption passphrase, the server login credentials, and the restore procedure — and you are hit by a bus, or you resign, or you are on holiday without signal — the organisation loses access to every credential it holds. Document the recovery process. Store the documentation and the recovery credentials in a sealed envelope in a physical safe that at least one other trusted person can access. Test the recovery process annually.

Client Setup

Every official Bitwarden client works with Vaultwarden without modification. Browser extensions for Firefox, Chrome, Edge, and Safari. Mobile apps for iOS and Android. Desktop applications for Windows, macOS, and Linux. The CLI tool for scripting and automation.

In any Bitwarden client, before logging in, open the settings and select the self-hosted server option. Enter your full domain — https://vault.example.com. The client connects to your server instead of Bitwarden's cloud. The experience is identical from that point forward.

Enforce two-factor authentication for every account. This is not optional for an organisation deployment. Vaultwarden supports TOTP authenticator apps — any app that generates time-based codes — email codes if SMTP is configured, YubiKey OTP, and WebAuthn/FIDO2 hardware security keys. Hardware security keys — YubiKey, SoloKey, or similar — are the strongest option. They are phishing-resistant and do not depend on a separate device having battery life.

Encourage users to export their vaults periodically as an encrypted backup. The Bitwarden client supports encrypted JSON exports protected by a separate password — not the master password. This gives individual users a personal recovery option independent of the server backup. Useful if the server is temporarily unavailable, if a database restore goes wrong, or if they need to migrate to a different Vaultwarden instance or back to Bitwarden's cloud.

Roll out the migration in stages if you are moving from another password manager. Start with the technical staff who understand the implications. Let them identify configuration issues, client quirks, and workflow gaps before the rest of the organisation moves over. A password manager migration that goes wrong — lost credentials, broken 2FA, locked-out accounts — erodes the trust you need users to place in the system.

Organisations using Vaultwarden's organisation feature — shared vaults for teams — should designate at least two organisation owners. A single owner who loses access, leaves the organisation, or is unavailable creates a single point of failure for every shared credential. The admin panel can reassign ownership, but only if someone can access it.

This deployment follows the same architectural pattern as the NextCloud tutorial — a containerised application running in Docker on Ubuntu, behind an Apache reverse proxy handling TLS termination, with a database backend for persistent storage and a firewall restricting network access. The pattern repeats because it works — it is a well-understood, well-tested architecture that separates concerns cleanly and hardens each layer independently.

Deploy Vaultwarden behind Apache with a valid TLS certificate, disable public registration after creating your accounts, configure Fail2Ban to block brute-force attempts at both the application and network layers, set up automated encrypted backups stored at a separate physical location, document the full recovery process for your successor, and enforce two-factor authentication on every account — your organisation's credentials deserve infrastructure you control.