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:
- C# for the core (ouch),
- Microsoft SQL Server for the database (ouch ouch).
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:
- Rust for the core (great),
- SQLite, MySQL or PostgreSQL for the database (very great).
Information and requirements
These elements are to be taken into consideration to follow this article:
- the manipulations are carried out on CentOS 8.2.2004,
- throughout this post, we will access the Bitwarden RS server via its domain name
vault.bitwarden.lan
which is related to its local IP address, - in some snippets of code, the domain name
vault.domain.fr
must be replaced by the one you assigned to your public Bitwarden RS server.
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/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/bitwarden_rs.git /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=127.0.0.1/" /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=0.0.0.0/WEBSOCKET_ADDRESS=127.0.0.1/" /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 https://github.com/bitwarden/web.git /tmp/vault
curl -Lo /tmp/vault/v2.18.0.patch -sSf https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/v2.18.0.patch
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/
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
.
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>
<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://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/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
.
[Unit]
Description=Bitwarden RS server
Documentation=https://github.com/dani-garcia/bitwarden_rs
After=network.target
[Service]
LimitMEMLOCK=infinity
LimitNOFILE=65535
LimitNPROC=64
RestartSec=2s
Type=simple
User=bitwarden
Group=bitwarden
WorkingDirectory=/etc/bitwarden
ExecStart=/usr/local/bin/bitwarden
Restart=always
EnvironmentFile=/etc/bitwarden/.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 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:
- create the user(s) who will have access to the application by hiting
Create Account
button and fill-in fields, - go to
/admin
(paste the token copied earlier in the field), - open the
General settings
ribbon, - uncheck
Allow new signups
, - uncheck
Allow invitations
, - uncheck
Show password hints
, - hit
Save
button.
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
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 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 README.md
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)
dir=/var/local/bitwarden/backups/$backuptime
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
.
[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/bitwarden-rs.local
.
[bitwarden-rs]
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
.
[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/bitwarden-admin.local
.
[bitwarden-rs-admin]
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 {
missingok
notifempty
compress
weekly
rotate 7
delaycompress
create 0644 bitwarden bitwarden
}
This configuration file contains:
missingok
: if the log file is missing, go on to the next one without issuing an error message,notifempty
: do not rotate the log if it is empty,compress
: old versions of log files are compressed withGzip
,weekly
: log files are rotated if the current weekday is less than the weekday of the last rotation or if more than a week has passed since the last rotation,rotate 7
: log files are rotated count times before being removed,delaycompress
: postpone compression of the previous log file to the next rotation cycle. It can be used when some program cannot be told to close its logfile and thus might continue writing to the previous log file for some time,create 0644 bitwarden bitwarden
: immediately after rotation, the log file is created with the same name as the log file just rotated.
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.