Install a Vaultwarden server

I’m pretty sure you’ve ever heard of Bitwarden. Bitwarden is a password manager. The big difference (among many others) compared to Dashlane is that you can deploy your own Bitwarden instance on your server in a Docker container, or do it from scratch but I think it’s pretty tricky…

The software stack is composed as follow:

These two Microsoft technologies convinced me to go elsewhere, but I got caught up with Vaultwarden!

Vaultwarden is a Bitwarden server API implementation written in Rust compatible with upstream Bitwarden clients, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.

The software stack is composed as follow:

Information and requirements

These elements are to be taken into consideration to follow this article:

Update the system

sudo dnf -y update

Install required utilities

sudo dnf -y install vim openssl-devel httpd mod_ssl epel-release python2 firewalld policycoreutils-python-utils
sudo dnf -y groupinstall 'Development Tools'

Create a system user to run Vaultwarden

sudo adduser --system --shell /bin/false --comment "Vaultwarden" --user-group -M vaultwarden

Install Rust

Note: for these two downloaded scripts, always check the content or checksums and then execute it. In other words, don’t do pipe curl to bash like many tutorials do.

The nightly version of Rust must be installed.

curl -Lo /tmp/sh.rustup.rs -sSf https://sh.rustup.rs
bash -E /tmp/sh.rustup.rs -y --default-host x86_64-unknown-linux-gnu --default-toolchain nightly --profile minimal
source ~/.cargo/env
rm -f /tmp/sh.rustup.rs

Install Node.JS and npm

curl -Lo /tmp/setup_14.x -sSf https://rpm.nodesource.com/setup_14.x
sudo bash -E /tmp/setup_14.x
sudo dnf -y install nodejs
rm -f /tmp/setup_14.x

Compile and configure the back-end

git clone https://github.com/dani-garcia/vaultwarden.git /tmp/vaultwarden
cargo build --features sqlite --release --manifest-path=/tmp/vaultwarden/Cargo.toml

Create the directory structure

sudo mkdir -p /var/lib/vaultwarden/{data,log}
sudo mkdir /etc/vaultwarden

Create the configuration file

cp /tmp/vaultwarden/.env.template /tmp/vaultwarden/.env

Enable the administration interface

This command uncomments the ADMIN_TOKEN variable in the configuration file and generates a token of 50 characters via /dev/urandom.

sed -i 's/'"# ADMIN_TOKEN=.*"'/'"ADMIN_TOKEN=$(tr -cd '[:alnum:]' < /dev/urandom | fold -w 49 | head -n 1)"'/' /tmp/vaultwarden/.env

Grab the token, we’ll need it later.

grep ^ADMIN_TOKEN /tmp/vaultwarden/.env

Set the web vault folder

sed -i "s/# WEB_VAULT_FOLDER=web-vault/WEB_VAULT_FOLDER=\/var\/www\/vault\/build/" /tmp/vaultwarden/.env

Change the Rocket IP address

Vaultwarden uses the web framework Rocket to publish API endpoints. Because we are going to use a reverse proxy, Rocket must serve pages on the server’s loopback address.

sed -i "s/# ROCKET_ADDRESS=0\.0\.0\.0.*/ROCKET_ADDRESS=127.0.0.1/" /tmp/vaultwarden/.env

Enable WebSocket notifications

sed -i "s/# WEBSOCKET_ENABLED=false/WEBSOCKET_ENABLED=true/" /tmp/vaultwarden/.env

Change the WebSocket IP address

sed -i "s/# WEBSOCKET_ADDRESS=0.0.0.0/WEBSOCKET_ADDRESS=127.0.0.1/" /tmp/vaultwarden/.env

Set the domain

The domain must match the address from where you access the server.

sed -i "s/# DOMAIN=.*/DOMAIN=https:\/\/vault.server.lan/" /tmp/vaultwarden/.env

Set the log file and path

From release 1.5.0, Vaultwarden supports logging to file. It is necessary to monitor what happens on the server. If you plan to use Fail2ban, you need to do the following thing.

sed -i "s/# LOG_FILE=.*/LOG_FILE=\/var\/lib\/vaultwarden\/log\/vaultwarden.log/" /tmp/vaultwarden/.env
sed -i "s/# LOG_LEVEL=.*/LOG_LEVEL=warn/" /tmp/vaultwarden/.env

LOG_LEVEL options are trace, debug, info, warn, error or off. The ones that still allows Fail2ban properly are warn and error.

Set data and database folders

sed -i "s/# DATA_FOLDER=data/DATA_FOLDER=\/var\/lib\/vaultwarden\/data/" /tmp/vaultwarden/.env
sed -i "s/# DATABASE_URL=data\/db.sqlite3/DATABASE_URL=\/var\/lib\/vaultwarden\/data\/db.sqlite3/" /tmp/vaultwarden/.env

Compile the front-end

git clone --branch v2.19.0 https://github.com/bitwarden/web.git /tmp/vault
curl -Lo /tmp/vault/v2.19.0.patch -sSf https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/v2.19.0.patch
git -C /tmp/vault apply /tmp/vault/v2.19.0.patch
npm run sub:init --prefix /tmp/vault
npm install --prefix /tmp/vault
npm audit fix --prefix /tmp/vault
npm run dist --prefix /tmp/vault

Move files and set permissions

Vaultwarden

sudo mv /tmp/vaultwarden/target/release/vaultwarden /usr/local/bin/vaultwarden
sudo mv /tmp/vaultwarden/.env /etc/vaultwarden
rm -rf /tmp/vaultwarden/
sudo chmod 750 /usr/local/bin/vaultwarden
sudo chown root:vaultwarden /usr/local/bin/vaultwarden
sudo chown -R vaultwarden:vaultwarden /var/lib/vaultwarden/
sudo chmod -R 750 /var/lib/vaultwarden/
sudo chown -R root:vaultwarden /etc/vaultwarden/
sudo chmod 770 /etc/vaultwarden/

Vault

sudo mv /tmp/vault/ /var/www/vault/
sudo chown -R apache:apache /var/www/vault/

Add authorized ports

sudo firewall-cmd --add-port={80/tcp,443/tcp} --permanent
sudo firewall-cmd --reload

Reverse proxy configurations

General TLS configurations

Add these headers and the cache configuration for OCSP stapling just before the default virtual host (<VirtualHost _default_:443>) into /etc/httpd/conf.d/ssl.conf.

# Headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy no-referrer

# OCSP stapling
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

Set the Diffie-Hellman parameters. First, you must generate the parameters. Here, the size of the generated parameter set will be 4096 bits.

sudo openssl dhparam -out /etc/pki/tls/private/dhparam.pem 4096
sudo chmod 440 /etc/pki/tls/private/dhparam.pem

Then, specify to Apache2 where the file is located via the DHParameters directive.

# Key exchange
SSLOpenSSLConfCmd DHParameters "/etc/pki/tls/private/dhparam.pem"

Disable TLS session tickets and enable the addition of OCSP responses to TLS negociation just before the end of the virtual host (</VirtualHost>) into /etc/httpd/conf.d/ssl.conf.

SSLSessionTickets Off
SSLUseStapling On

Accept all protocols except those before TLSv1.2.

sudo sed -i "s/#SSLProtocol.*/SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1/" /etc/httpd/conf.d/ssl.conf

Generate a self-signed certificate (local server only)

Generate a private key for a curve. Here, I use the curve secp521r1 but you can use another one by executing openssl ecparam -list_curves.

sudo openssl ecparam -check -name secp521r1 -genkey -noout -out /etc/pki/tls/private/privkey.pem -rand /dev/urandom
checking elliptic curve parameters: ok

You can set the temporary curve used for ephemeral ECDH modes. You can use the same curve you just used to generate the private key. Add the following statement juste after the SSLOpenSSLConfCmd DHParameters directive into /etc/httpd/conf.d/ssl.conf.

SSLOpenSSLConfCmd ECDHParameters secp521r1

Create a self-signed certificate. Feel free to personalize the answers to the questions asked. This self-signed certificate is used for testing purposes so it is not essential that the information given is correct.

sudo openssl req -utf8 -new -x509 -key /etc/pki/tls/private/privkey.pem -out /etc/pki/tls/certs/cert.pem -days 365 -rand /dev/urandom
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letters code) [XX]:FR
State or Province Name (full name) []:Rhone-Alpes
Locality Name (eg, city) [Default City]:Lyon
Organizational Name (eg, company) [Default Company Ltd]:Vaultwarden
Organizational Unit Name (eg, section) []:IT Department
Common Name (eg, your name or your server's hostname) []:vault.server.lan
Email Address []:contact@server.lan

Set the correct permission.

sudo chmod 440 /etc/pki/tls/private/privkey.pem
sudo chmod 644 /etc/pki/tls/certs/cert.pem

Then, create the reverse proxy configuration into /etc/httpd/conf.d/vhost.conf.

<VirtualHost *:80>
    ServerName vault.server.lan:80
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName vault.server.lan:443
    ServerAlias vault.server.lan
    ServerAdmin contact@server.lan

    SSLCertificateFile /etc/pki/tls/certs/cert.pem
    SSLCertificateKeyFile /etc/pki/tls/private/privkey.pem
    SSLCACertificateFile /etc/pki/tls/certs/cert.pem

    Protocols h2 http/1.1

    ErrorLog /var/log/vaultwarden/error_log
    CustomLog /var/log/vaultwarden/access_log combined

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /notifications/hub(.*) ws://127.0.0.1:3012/ [P,L]
    ProxyPass / http://127.0.0.1:8000/

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s
</VirtualHost>

Request a Let’s Encrypt certificate (public server only)

In case you wish to obtain free certificates (via Let’s Encrypt), two methods are available to you. Luck is on your side, you will find here the method to obtain a certificate from Let’s Encrypt, and here a slightly more advanced method that allows you to obtain a certificate based on elliptic curve cryptography (still via Let’s Encrypt).

The reverse proxy configuration is the exact same. Things you have to modify are the location of the certificate, private key and the chain of trust, but it is documented in the article previously mentionned.

Create the logs directory

sudo mkdir -p /var/log/vaultwarden

Enable and start HTTPD service

sudo systemctl enable --now httpd.service

Create the service file

Copy the following snippet into /etc/systemd/system/vaultwarden.service.

[Unit]
Description=Vaultwarden
Documentation=https://github.com/dani-garcia/vaultwarden
After=network.target

[Service]
LimitMEMLOCK=infinity
LimitNOFILE=65535
LimitNPROC=64
RestartSec=2s
Type=simple
User=vaultwarden
Group=vaultwarden
WorkingDirectory=/etc/vaultwarden
ExecStart=/usr/local/bin/vaultwarden
Restart=always
EnvironmentFile=/etc/vaultwarden/.env
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Restart systemctl daemon

sudo systemctl daemon-reload

SELinux stuff

If SELinux is enabled Vaultwarden will not start. You must change the SELinux content of the binary.

sudo semanage fcontext -a -t bin_t /usr/local/bin/vaultwarden
sudo restorecon -v /usr/local/bin/vaultwarden

You’re still not quite done with SELinux, you must allow HTTPD scripts and modules to connect to the network.

sudo setsebool -P httpd_can_network_connect on

The -P flag allows to make this modification persistent even after a reboot.

Enable and start Vaultwarden service

sudo systemctl enable --now vaultwarden.service

Final configurations

Go to https://vault.server.lan (or the domain name that you have linked to your Vaulwarden’s public IP address) and follow these steps:

Disable the administration interface

It is necessary to comment this variable and at the same time remove the token associated with it for security reasons.

sudo sed -i "s/^ADMIN_TOKEN=.*/# ADMIN_TOKEN=/" /etc/vaultwarden/.env

Because Vaultwarden has a runtime configuration file, commenting the ADMIN_TOKEN variable is not enough to disable the administration interface. To do this you must also delete the admin_token variable in this file.

sudo sed -i '/"admin_token/d' /var/lib/vaultwarden/data/config.json

Then, restart the server.

sudo systemctl restart vaultwarden.service

Recommendations

Generate strong password

Using a password manager is good, but storing weak passwords is useless. Generating a strong password can be done like follow. You can easily modify the password length by increasing or decreasing the -w flag value.

tr -cd [:alnum:] < /dev/urandom | fold -w 49 | head -n 1

And now, with special characters.

tr -cd [:graph:] < /dev/urandom | fold -w 49 | head -n 1

Update Vaultwarden

You will find here a script I wrote to update the Vaultwarden binary. The steps of implementation are explained in the README.md file.

YubiKey OTP authentication

Work in progress.

Backing up your Vaultwarden SQLite database

You will find here a script I wrote to save the Vaultwarden SQLite database. The steps of implementation are explained in the README.md file.

Protection against brute-force with Fail2ban

sudo dnf -y install fail2ban-server fail2ban-firewalld

First, we want to ban brute-force user login attempts. Copy the following snippet into /etc/fail2ban/filter.d/vaultwarden.conf.

[INCLUDES]
before = common.conf

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

Create the jail by copying the following snippet into /etc/fail2ban/jail.d/vaultwarden.local.

[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
action = iptables-allports[name=vaultwarden]
logpath = /var/lib/vaultwarden/log/vaultwarden.log
maxretry = 3
bantime = 86400
findtime = 3600

Although we have disabled the administration page, if you decide to enable it to modify the configuration and forget to disable it again, you’ll protect your server from brute-force attack. Copy the following snippet into /etc/fail2ban/filter.d/vaultwarden-admin.conf.

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Invalid admin token\. IP: <ADDR>.*$
ignoreregex =

Same as before, create the jail by copying the following snippet into /etc/fail2ban/jail.d/vaultwarden-admin.local.

[vaultwarden-admin]
enabled = true
port = 80,443
filter = vaultwarden-admin
action = iptables-allports[name=vaultwarden-admin]
logpath = /var/lib/vaultwarden/log/vaultwarden.log
maxretry = 3
bantime = 86400
findtime = 3600

Once again, if SELinux is enabled Fail2ban will not start. You must change the SELinux content of the log file.

sudo semanage fcontext -a -t var_log_t /var/lib/vaultwarden/log/vaultwarden.log
sudo restorecon -v /var/lib/vaultwarden/log/vaultwarden.log

Start the Fail2ban server and enable it at boot.

sudo systemctl enable --now fail2ban.service

Check the standard login attempts jail.

sudo fail2ban-client status vaultwarden
Status for the jail: vaultwarden
|- Filter
|  |- Currently failed: 0
|  |- Total failed:	0
|  `- File list:	/var/lib/vaultwarden/log/vaultwarden.log
|
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Check the admin login attempts jail.

sudo fail2ban-client status vaultwarden-admin
Status for the jail: vaultwarden-admin
|- Filter
|  |- Currently failed: 0
|  |- Total failed:	0
|  `- File list:	/var/lib/vaultwarden/log/vaultwarden.log
|
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

You can try to log in with bad credentials and executed a new time the previous command, the Currently failed and Total failed values will increment. Don’t abuse, you’ll be blocked otherwise!

Finally, you can unban an IP with the following command.

sudo fail2ban-client set vaultwarden{-admin} unbanip <IP>

Setup logs rotation

sudo dnf -y install logrotate

Copy the following snippet into /etc/logrotate.d/vaultwarden.

/var/lib/vaultwarden/log/vaultwarden.log {
	missingok
	notifempty
	compress
	weekly
	rotate 7
	delaycompress
	create 0644 vaultwarden vaultwarden
}

This configuration file contains:

You can execute a dry-run to see what logrotate would do if it was actually executed now.

sudo logrotate -d /etc/logrotate.d/vaultwarden

Finally, logrotate creates an entry in /etc/cron.daily/logrotate to execute the configured tasks.