Install a Bitwarden RS 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 Bitwarden RS server!

Bitwarden RS server 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 Bitwarden RS server

sudo adduser --system --shell /bin/false --comment "Bitwarden RS server" --user-group -M bitwarden

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/ -sSf
bash -E /tmp/ -y --default-host x86_64-unknown-linux-gnu --default-toolchain nightly --profile minimal
source ~/.cargo/env
rm -f /tmp/

Install Node.JS and npm

curl -Lo /tmp/setup_14.x -sSf
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 /tmp/bitwarden
cargo build --features sqlite --release --manifest-path=/tmp/bitwarden/Cargo.toml

Create the directory structure

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

Create the configuration file

cp /tmp/bitwarden/.env.template /tmp/bitwarden/.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/bitwarden/.env

Grab the token, we’ll need it later.

grep ^ADMIN_TOKEN /tmp/bitwarden/.env

Set the web vault folder

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

Change the Rocket IP address

Bitwarden RS server 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=" /tmp/bitwarden/.env

Enable WebSocket notifications

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

Change the WebSocket IP address

sed -i "s/# WEBSOCKET_ADDRESS=" /tmp/bitwarden/.env

Set the domain

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

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

Set the log file and path

From release 1.5.0, Bitwarden RS server 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\/bitwarden\/log\/bitwarden.log/" /tmp/bitwarden/.env
sed -i "s/# LOG_LEVEL=.*/LOG_LEVEL=warn/" /tmp/bitwarden/.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\/bitwarden\/data/" /tmp/bitwarden/.env
sed -i "s/# DATABASE_URL=data\/db.sqlite3/DATABASE_URL=\/var\/lib\/bitwarden\/data\/db.sqlite3/" /tmp/bitwarden/.env

Compile the front-end

git clone --branch v2.18.1 /tmp/vault
curl -Lo /tmp/vault/v2.18.0.patch -sSf
git -C /tmp/vault apply /tmp/vault/v2.18.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

Bitwarden RS server

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


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.

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

SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

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

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]:Bitwarden RS
Organizational Unit Name (eg, section) []:IT Department
Common Name (eg, your name or your server's hostname) []:vault.bitwarden.lan
Email Address []:contact@bitwarden.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.bitwarden.lan:80
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

<VirtualHost *:443>
    ServerName vault.bitwarden.lan:443
    ServerAlias vault.bitwarden.lan
    ServerAdmin contact@bitwarden.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/bitwarden/error_log
    CustomLog /var/log/bitwarden/access_log combined

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

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s

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/bitwarden

Enable and start HTTPD service

sudo systemctl enable --now httpd.service

Create the service file

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

Description=Bitwarden RS server



Restart systemctl daemon

sudo systemctl daemon-reload

SELinux stuff

If SELinux is enabled Bitwarden RS server will not start. You must change the SELinux content of the binary.

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

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 Bitwarden RS server service

sudo systemctl enable --now bitwarden.service

Final configurations

Go to https://vault.bitwarden.lan (or the domain name that you have linked to your Bitwarden RS server’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/bitwarden/.env

Because Bitwarden RS server 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/bitwarden/data/config.json

Then, restart the server.

sudo systemctl restart bitwarden.service


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 Bitwarden RS server

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

Yubikey OTP authentication

Work in progress.

Backing up your vault

Install the required utility and create the folder that will receive the backups.

sudo dnf -y install sqlite
sudo mkdir -p /var/local/bitwarden/backups

Copy the following snippet into /usr/local/sbin/bwbackup.

#! /usr/bin/env bash

backuptime=$(date +%Y-%m-%d_%I-%M-%S)
mkdir -p $dir

/usr/bin/sqlite3 /var/lib/bitwarden/data/db.sqlite3 ".backup $dir/backup.sqlite3"

Set the correct permissions

sudo chmod 750 /usr/local/sbin/bwbackup

Then, add these entries into the root’s user crontab (sudo crontab -e).

*/15 * * * * /usr/local/sbin/bwbackup
*/16 * * * * /usr/bin/find /var/local/bitwarden/backups/ -type d -mmin +60 -exec rm -rf {} \;

Each 15 minutes, the script will save the database in a directory (format: Y-m-d_H-M-S) into /var/local/bitwarden/backups/. Each 16 minutes, all backup older than 1 hour will be deleted.

Restore a backup

Replace <date> with the folder name (the date you want to restore the data).

sudo systemctl stop bitwarden.service
sudo cp /var/local/bitwarden/backups/<date>/backup.sqlite3 /var/lib/bitwarden/data/db.sqlite3
sudo systemctl start bitwarden.service

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/bitwarden-rs.conf.

before = common.conf

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/bitwarden-rs.local.

enabled = true
port = 80,443
filter = bitwarden-rs
action = iptables-allports[name=bitwarden-rs]
logpath = /var/lib/bitwarden/log/bitwarden.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/bitwarden-rs-admin.conf.

before = common.conf

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

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

enabled = true
port = 80,443
filter = bitwarden-rs-admin
action = iptables-allports[name=bitwarden-rs-admin]
logpath = /var/lib/bitwarden/log/bitwarden.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/bitwarden/log/bitwarden.log
sudo restorecon -v /var/lib/bitwarden/log/bitwarden.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 bitwarden-rs
Status for the jail: bitwarden-rs
|- Filter
|  |- Currently failed: 0
|  |- Total failed:	0
|  `- File list:	/var/lib/bitwarden/log/bitwarden.log
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Check the admin login attempts jail.

sudo fail2ban-client status bitwarden-rs-admin
Status for the jail: bitwarden-rs-admin
|- Filter
|  |- Currently failed: 0
|  |- Total failed:	0
|  `- File list:	/var/lib/bitwarden/log/bitwarden.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 bitwarden-rs{-admin} unbanip <IP>

Setup logs rotation

sudo dnf -y install logrotate

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

/var/lib/bitwarden/log/bitwarden.log {
	rotate 7
	create 0644 bitwarden bitwarden

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/bitwarden

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