April 27, 2021

Vaultwarden, Docker-Compose & Caddy V2

Bitwarden is a highly regarded free & open source password manager.

Vaultwarden (formerly "Bitwarden RS") is a light-weight implementation of Bitwarden written in Rust, providing most of the functionality, including FIDO2/U2F hardware security key support!

This guide will cover installing, configuring and hardening Vaultwarden on Ubuntu 20.04 using Docker Compose, with Caddy V2 as reverse proxy.

We'll also cover one way to automate backing up your vault's SQLite database to a Nextcloud instance using Python!

We assume you have:

  • Basic Linux / sysadmin skills
  • Ubuntu 20.04 with sudo
  • Access to a domain name

Docker Compose

Please be sure the latest Docker and Docker Compose are correctly installed.

We'll use the folder /home/YourUser/Docker to store the Docker Compose configuration files - but you can use whatever you prefer. Let's get started!

First, create the directories and docker-compose.yml:

mkdir /home/YourUser/Docker && cd /home/YourUser/Docker
mkdir Vaultwarden && cd Vaultwarden
sudo nano docker-compose.yml

Paste in the Vaultwarden configuration below, enter your own domain name and make any other desired changes:

version: '3'

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: Vaultwarden
    restart: always
    environment:
      - DOMAIN=https://vw.yourdomain.com
      - LOG_FILE=/data/vaultwarden.log
      - LOG_LEVEL=warn
      - EXTENDED_LOGGING=true
    volumes:
      - /vw-data:/data/
    ports:
      - 8123:80

Optionally, you may configure Vaultwarden for SMTP to receive notifications about new logins and send signup invitations.

Important: make sure your domain registrar points vw.yourdomain.com to your server's IP address!

Start the container with:

docker-compose up -d

You should end up with something like this:

Pulling vaultwarden (vaultwarden/server:latest)...
latest: Pulling from vaultwarden/server
f7ec5a41d630: Pull complete
a2cf0912fe38: Pull complete
3a24a82c782a: Pull complete
629a39b3ade8: Pull complete
6270d78fa5ef: Pull complete
59acdef23b47: Pull complete
57f0cbe88d79: Pull complete
a662ea2b469c: Pull complete
Digest: sha256:8324a196cca8369373309553f3702e2c2c716de0c71caa3761c57d23bc28b933
Status: Downloaded newer image for vaultwarden/server:latest
Creating Vaultwarden ... done

At this point, your Vaultwarden instance should be reachable at localhost:8123!

Updating Vaultwarden

If you prefer manual updates, use the following commands:

cd /home/YourUser/Docker/Vaultwarden
docker-compose stop
docker-compose pull
docker-compose start

If you prefer automatic updates, use Watchtower. Create another folder and docker-compose.yml for it:

cd /home/YourUser/Docker && mkdir Watchtower
cd Watchtower && sudo nano docker-compose.yml

Paste Watchtower configuration:

version: "3"
services:
  watchtower:
    image: containrrr/watchtower
    container_name: Watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 30

Start Watchtower:

docker-compose up -d

Watchtower will now monitor and automatically gracefully shut down, update and restart your docker containers using the same options they were started with!

Next, we'll set up Caddy to get your Vaultwarden instance available at your domain.

Install & Configure Caddy

Caddy will help simplify the process of setting up a reverse proxy with HTTPS.

Use the following commands to install Caddy:

sudo apt install curl
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Now we'll head over to the Caddyfile:

cd /etc/caddy && sudo nano Caddyfile

Paste in the configuration (remember: the domain and port here must match your Vaultwarden docker-compose.yml):

vw.yourdomain.com {
    reverse_proxy localhost:8123 {
        header_up X-Real-IP {remote_host}
    }
    encode gzip
    file_server
}

Have Caddy load your new Vaultwarden configuration:

caddy reload

Your Vaultwarden instance is now working at https://vw.yourdomain.com!

Note: we highly recommend using Vaultwarden with a FIDO2/U2F hardware security key, such as Nitrokey or Yubikey, which is supported by default in Vaultwarden when configured as shown in this guide.

You can stop here and enjoy your fresh Vaultwarden instance, or you can read on to learn how to harden your setup against brute force attacks and automate backing up your vault.

Fail2ban: Protection Against Brute-Force Attacks

While Vaultwarden is already very secure by default, it's always prudent to layer in some extra security - don't be that low hanging fruit! Using fail2ban provides protection against brute force attacks by automatically blocking any IP that has consecutive failed login attempts.

This section will cover installing and configuring fail2ban to work with Vaultwarden.

Install fail2ban:

sudo apt-get install fail2ban -y

Create a fail2ban filter file for Vaultwarden:

sudo nano /etc/fail2ban/filter.d/vaultwarden.local

Paste in the following configuration:

[INCLUDES]
before = common.conf

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

Create a fail2ban jail file for Vaultwarden:

sudo nano /etc/fail2ban/jail.d/vaultwarden.local

Paste in the following configuration:

[vaultwarden]
enabled = true
port = 80,443,8123
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

After saving the configuration, restart fail2ban:

sudo systemctl reload fail2ban

To confirm fail2ban is working, browse to your Vaultwarden instance and try logging in with incorrect login credentials at least 3 times consecutively. If it's working, the page should stop responding and no longer load at all anymore.

To check the Vaultwarden log for failed login attempts:

tail /vw-data/vaultwarden.log

You should see output that looks something like this:

[2021-04-27 21:27:57.788][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: 72.142.218.131. Username: l33th4x0r.
[2021-04-27 21:27:58.874][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: 72.142.218.131. Username: l33th4x0r.
[2021-04-27 21:27:59.902][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: 72.142.218.131. Username: l33th4x0r.

If you're connecting from your local / home network, you may see an IP address like 192.168.1.XX instead. If you repeat the same steps from an outside IP (say, through a VPN, or connecting from the data connection on a mobile device) then you should see a normal external / public IP listed there.

To unblock a blocked IP, do:

sudo fail2ban-client set vaultwarden unbanip XX.XX.XX.XX

You now have a fairly well rounded setup! All that's left is to sort out a backup solution, so that if anything happens to go wrong, you can always quickly restore your vault.

Automate Vaultwarden Backups to Nextcloud

For some extra peace of mind, we'll use a small Python script to automatically back up Vaultwarden's config and database to Nextcloud. The script will use webdavclient, which will help simplify the process.

Install webdavclient by doing:

pip3 install webdavclient

SQLite3 is required to safely back up the database, make sure it's installed:

sudo apt install sqlite3

You can store the script anywhere, here we'll use /home/YourUser/Scripts:

cd /home/YourUser && mkdir Scripts
cd Scripts && sudo nano vw-backup.py

Before you go any further, you'll need to log into your Nextcloud account, go to Settings -> Personal -> Security, scroll down to the bottom and enter an app name like vw-backup then click on Create new app password.

Paste the code below into vw-backup.py and fill out the following webdavconfig fields:

  • webdav_hostname webdav address for your Nextcloud user
  • webdav_login Nextcloud username
  • webdav_password Nextcloud app password
import webdav.client, os, time

# configure and initialize webdavclient
webdavconfig = {'webdav_hostname': "https://cloud.yourdomain.com/remote.php/dav/files/YourUser/", 'webdav_login': "YourUser", 'webdav_password': "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx", 'webdav_root': "/"}
wc = webdav.client.Client(webdavconfig)

# create local backup folder if it doesn't exist
if not os.path.isdir('/vw-data/vw-backup'):
    os.mkdir('/vw-data/vw-backup')

# create Nextcloud vw-backup folder if it doesn't exist
if not wc.check("vw-backup"):
    wc.mkdir("vw-backup")

# create datestamp and local backup commands
datestamp = time.strftime("%Y-%m-%d")
backupdb = f'''sqlite3 /vw-data/db.sqlite3 ".backup '/vw-data/vw-backup/db-{datestamp}.sqlite3'"'''
backupconfig = f'cp /vw-data/config.json /vw-data/vw-backup/config.json-{datestamp}'

# run the local backup commands
os.system(backupdb)
os.system(backupconfig)

# define the local backup and Nextcloud backup filepaths
sourcedb = f'/vw-data/vw-backup/db-{datestamp}.sqlite3'
targetdb = f'/vw-backup/db-{datestamp}.sqlite3'
sourceconfig = f'/vw-data/vw-backup/config.json-{datestamp}'
targetconfig = f'/vw-backup/config.json-{datestamp}'

# upload backup files to Nextcloud
wc.upload_sync(remote_path=targetdb, local_path=sourcedb)
wc.upload_sync(remote_path=targetconfig, local_path=sourceconfig)

Now do a test run to make sure the script is working as expected:

python3 vw-backup.py

You should now see two files that look something like config.json-2021-04-28 and db-2021-04-28.sqlite3 within the folder /vw-data/vw-backup, and you should also be able to confirm the same have appeared in the /vw-backup folder in Nextcloud!

The last step is to have the script run automatically. Here we'll use cron to have our script run daily at midnight. Do:

crontab -e

And add:

@midnight /usr/bin/python3 /home/YourUser/Scripts/vw-backup.py

If you'd like to do a test run there first, do this instead:

* * * * * /usr/bin/python3 /home/YourUser/Scripts/vw-backup.py

That will make the script run once every minute, so you can confirm that the backups are indeed happening, and then set cron back to @midnight.

To check when the script last run, do:

systemctl status cron | grep vw-backup

Congratulations, you now have a highly secure self-hosted free & open source password manager that updates and backs itself up automatically!

We hope this guide has been helpful! For even more information on configuring Vaultwarden, see their Github wiki.