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

[operator@vault ~]$ sudo dnf -y update

Install required utilities

[operator@vault ~]$ sudo dnf -y install vim openssl-devel httpd mod_ssl epel-release firewalld policycoreutils-python-utils git gcc tar

Create a system user to run Vaultwarden

[operator@vault ~]$ sudo adduser -r -s /bin/false -c "Vaultwarden server" -U -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.

[operator@vault ~]$ curl -LsSf https://sh.rustup.rs -o rustup-init.sh
[operator@vault ~]$ bash -E rustup-init.sh -y --default-host x86_64-unknown-linux-gnu --default-toolchain nightly --profile minimal
[operator@vault ~]$ source $HOME/.cargo/env
[operator@vault ~]$ rm -f rustup-init.sh
[operator@vault ~]$ rustc --version
rustc 1.65.0-nightly (40336865f 2022-08-15)

Compile and configure the back-end

[operator@vault ~]$ git clone --depth 1 https://github.com/dani-garcia/vaultwarden.git /tmp/vaultwarden
[operator@vault ~]$ cargo build --features sqlite --release --manifest-path=/tmp/vaultwarden/Cargo.toml

Create the directory structure

[operator@vault ~]$ sudo mkdir -p /var/lib/vaultwarden/{data,log}
[operator@vault ~]$ sudo mkdir -p /etc/vaultwarden

Create the configuration file

[operator@vault ~]$ 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.

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

Grab the token, we’ll need it later.

[operator@vault ~]$ grep ^ADMIN_TOKEN /tmp/vaultwarden/.env

Set the web vault folder

[operator@vault ~]$ sed -i "s/# WEB_VAULT_FOLDER=web-vault/WEB_VAULT_FOLDER=\/var\/www\/web-vault/" /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.

[operator@vault ~]$ sed -i "s/# ROCKET_ADDRESS=0\.0\.0\.0.*/ROCKET_ADDRESS=127.0.0.1/" /tmp/vaultwarden/.env

Enable WebSocket notifications

[operator@vault ~]$ sed -i "s/# WEBSOCKET_ENABLED=false/WEBSOCKET_ENABLED=true/" /tmp/vaultwarden/.env

Change the WebSocket IP address

[operator@vault ~]$ 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.

[operator@vault ~]$ sed -i "s/# DOMAIN=.*/DOMAIN=https:\/\/vault.local/" /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.

[operator@vault ~]$ sed -i "s/# LOG_FILE=.*/LOG_FILE=\/var\/lib\/vaultwarden\/log\/vaultwarden.log/" /tmp/vaultwarden/.env
[operator@vault ~]$ 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

[operator@vault ~]$ sed -i "s/# DATA_FOLDER=data/DATA_FOLDER=\/var\/lib\/vaultwarden\/data/" /tmp/vaultwarden/.env
[operator@vault ~]$ sed -i "s/# DATABASE_URL=data\/db.sqlite3/DATABASE_URL=\/var\/lib\/vaultwarden\/data\/db.sqlite3/" /tmp/vaultwarden/.env

Set trash auto delete

Number of days to wait before auto-deleting a trashed item. This parameter has a global scope, means that it applies to all users.

[operator@vault ~]$ sed -i "s/# TRASH_AUTO_DELETE_DAYS=.*/TRASH_AUTO_DELETE_DAYS=10/" /tmp/vaultwarden/.env

Install the front-end

Be sure to get the latest version available here.

[operator@vault ~]$ curl -LOsSf https://github.com/dani-garcia/bw_web_builds/releases/download/v2022.6.2/bw_web_v2022.6.2.tar.gz
[operator@vault ~]$ sudo tar -xvzf bw_web_v2022.6.2.tar.gz -C /var/www/
[operator@vault ~]$ rm -f bw_web_v2022.6.2.tar.gz

Move files and set permissions

Vaultwarden

[operator@vault ~]$ sudo mv /tmp/vaultwarden/target/release/vaultwarden /usr/local/bin/vaultwarden
[operator@vault ~]$ sudo mv /tmp/vaultwarden/.env /etc/vaultwarden
[operator@vault ~]$ rm -rf /tmp/vaultwarden/
[operator@vault ~]$ sudo chmod 750 /usr/local/bin/vaultwarden
[operator@vault ~]$ sudo chown root:vaultwarden /usr/local/bin/vaultwarden
[operator@vault ~]$ sudo chown -R vaultwarden:vaultwarden /var/lib/vaultwarden/
[operator@vault ~]$ sudo chmod -R 750 /var/lib/vaultwarden/
[operator@vault ~]$ sudo chown -R root:vaultwarden /etc/vaultwarden/
[operator@vault ~]$ sudo chmod 750 /etc/vaultwarden/
[operator@vault ~]$ sudo chmod 640 /etc/vaultwarden/.env

Vault

[operator@vault ~]$ sudo chown -R apache: /var/www/web-vault/

Add authorized ports

[operator@vault ~]$ sudo firewall-cmd --add-port={80/tcp,443/tcp} --permanent
[operator@vault ~]$ sudo firewall-cmd --reload

Reverse proxy configurations

General TLS configurations

By adding ServerTokens Prod directive, you remove the Apache2 version from HTTP headers, it’s a good practice for security reasons.

[operator@vault ~]$ sudo sed -i "352i ServerTokens Prod" /etc/httpd/conf/httpd.conf

Add these headers and the cache configuration for OCSP stapling just after #SSLCryptoDevice ubsec into /etc/httpd/conf.d/ssl.conf. Also, set the Diffie-Hellman parameters’ path and the temporary curve used for ephemeral ECDH modes.

# Headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
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)"

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

Next, you must generate the Diffie-Hellman’s parameters. Here, the size of the generated parameters set will be 4096 bits.

[operator@vault ~]$ sudo openssl dhparam -check -out /etc/pki/tls/private/dhparam.pem -rand /dev/urandom 4096
DH parameters appear to be ok.

Set correct permissions.

[operator@vault ~]$ sudo chmod 440 /etc/pki/tls/private/dhparam.pem

Disable TLS session tickets and enable the addition of OCSP responses to TLS negociation.

[operator@vault ~]$ sudo sed -i "214i SSLSessionTickets Off" /etc/httpd/conf.d/ssl.conf
[operator@vault ~]$ sudo sed -i "215i SSLUseStapling On" /etc/httpd/conf.d/ssl.conf

Accept all protocols except those before TLSv1.2.

[operator@vault ~]$ sudo sed -i "s/#SSLProtocol.*/SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1/" /etc/httpd/conf.d/ssl.conf

Generate TLS certificate (local server only)

Generate the CA private key

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.

[operator@vault ~]$ sudo openssl ecparam -check -name secp521r1 -genkey -noout -out /etc/pki/tls/private/CAkey.pem -rand /dev/urandom
checking elliptic curve parameters: ok

Generate the CA certificate

The CA root certificate will have 3650 days (10 years lifetime).

[operator@vault ~]$ sudo openssl req -utf8 -new -x509 -days 3650 -key /etc/pki/tls/private/CAkey.pem -out /etc/pki/tls/certs/CAcert.pem -rand /dev/urandom -subj "/CN=Vaultwarden root CA/"

Generate Vaultwarden private key

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

[operator@vault ~]$ sudo openssl ecparam -check -name secp384r1 -genkey -noout -out /etc/pki/tls/private/privkey.pem -rand /dev/urandom
checking elliptic curve parameters: ok

Create a dedicated OpenSSL configuration file

Copy the following content in a file called openssl.cnf. In CN = and DNS.1 =, be sure to put the Vaultwarden domain name, heere vault.local.

[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[req_distinguished_name]
CN = vault.local

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = vault.local

Generate CSR based on the OpenSSL configuration file

[operator@vault ~]$ sudo openssl req -new -key /etc/pki/tls/private/privkey.pem -out /tmp/vault.local.csr -rand /dev/urandom -config openssl.cnf

Generate the X.509 certificate with SAN

Certificate will have 365 days (1 year lifetime).

[operator@vault ~]$ sudo openssl x509 -req -days 365 -in /tmp/vault.local.csr -CA /etc/pki/tls/certs/CAcert.pem -CAkey /etc/pki/tls/private/CAkey.pem -CAcreateserial -out /etc/pki/tls/certs/vault.local.pem -rand /dev/urandom -extensions req_ext -extfile openssl.cnf
Signature ok
subject=CN = vault.local
Getting CA Private Key

Check the SAN extension

[operator@vault ~]$ openssl x509 -in /etc/pki/tls/certs/vault.local.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:vault.local

Don’t forget to add the CA root certificate to your certificate store so that the certificate linked to the vault.local domain name is validateur without error. You can retrieve it via this command.

[operator@vault ~]$ cat /etc/pki/tls/certs/CAcert.pem

This command should return something like this.

-----BEGIN CERTIFICATE-----
MIICGDCCAXqgAwIBAgIUQgDsGWjMPNXKW5/UMCktuaKhVyswCgYIKoZIzj0EAwIw
HjEcMBoGA1UEAwwTVmF1bHR3YXJkZW4gcm9vdCBDQTAeFw0yMjA4MTYyMTUzMDla
Fw0zMjA4MTMyMTUzMDlaMB4xHDAaBgNVBAMME1ZhdWx0d2FyZGVuIHJvb3QgQ0Ew
gZswEAYHKoZIzj0CAQYFK4EEACMDgYYABABybOIbMe2lhbOqn9lZdBw+ykRIUCKm
Nyn++qiqCCo9nxgQJcTQAFhXer4yWCEFdgwZRVA+TP9kzm0P+FsRK/K1sgG+yB+V
zYtopf35V1R17JbLfCeZQO7DG13pYid9mZ+mw0R2J3zvlogK8JClWGuEyQxiGsZ2
YLvvN+ioP9u5TWIAlqNTMFEwHQYDVR0OBBYEFB1eg9BlGT5J3yfti0GptpdwtgWI
MB8GA1UdIwQYMBaAFB1eg9BlGT5J3yfti0GptpdwtgWIMA8GA1UdEwEB/wQFMAMB
Af8wCgYIKoZIzj0EAwIDgYsAMIGHAkIBZCEBcNDrZei7ZxAoFeQG7Aek1g/Szp4O
UxNhMWbe7HqFEyRcn2PDN4s57hqB2GrfkcnBBUxov7lqmFa5KOu9L1ECQTwYqX0d
Gb3phhAYQQK7Q8Y6fpKjFOuN5YZzxmqygX9oN7YV4mvvJFhJs/g/9/o/ysafuCLY
ltG3OrAq7cDPjLWy
-----END CERTIFICATE-----

Copy this text and paste it in a file named CAcert.pem, this is the file you have to import in your trust store.

Set correct permissions

[operator@vault ~]$ sudo chmod 440 /etc/pki/tls/private/CAkey.pem
[operator@vault ~]$ sudo chmod 644 /etc/pki/tls/certs/CAcert.pem
[operator@vault ~]$ sudo chmod 440 /etc/pki/tls/private/privkey.pem
[operator@vault ~]$ sudo chmod 644 /etc/pki/tls/certs/vault.local.pem

Virtual host configuration

Create the reverse proxy configuration into /etc/httpd/conf.d/vhost.conf.

NameVirtualHost *:80

<VirtualHost *:80>
    ServerName 192.168.2.251
    Redirect permanent / https://vault.local
</VirtualHost>

NameVirtualHost *:443

<VirtualHost *:443>
    ServerName 192.168.2.251
    Redirect permanent / https://vault.local
</VirtualHost>

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

    SSLCertificateFile /etc/pki/tls/certs/vault.local.pem
    SSLCertificateKeyFile /etc/pki/tls/private/privkey.pem
    SSLCACertificateFile /etc/pki/tls/certs/CAcert.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

[operator@vault ~]$ sudo mkdir -p /var/log/vaultwarden

Enable and start HTTPD service

[operator@vault ~]$ sudo systemctl enable --now httpd

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

[operator@vault ~]$ sudo systemctl daemon-reload

SELinux stuff

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

[operator@vault ~]$ 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.

[operator@vault ~]$ 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

[operator@vault ~]$ sudo systemctl enable --now vaultwarden

Final configurations

Go to https://vault.local (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.

[operator@vault ~]$ 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.

[operator@vault ~]$ sudo sed -i '/"admin_token/d' /var/lib/vaultwarden/data/config.json

Then, restart the server.

[operator@vault ~]$ sudo systemctl restart vaultwarden

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.

[operator@vault ~]$ tr -c -d [:alnum:] < /dev/urandom | fold -w 49 | head -n 1

And now, with special characters.

[operator@vault ~]$ tr -c -d [: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

[operator@vault ~]$ 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 log folder’s SELinux context.

[operator@vault ~]$ sudo semanage fcontext -a -t var_log_t /var/lib/vaultwarden/log/vaultwarden.log
[operator@vault ~]$ sudo restorecon -v /var/lib/vaultwarden/log/vaultwarden.log

Start the Fail2ban server and enable it at boot.

[operator@vault ~]$ sudo systemctl enable --now fail2ban

Check the standard login attempts jail.

[operator@vault ~]$ 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.

[operator@vault ~]$ 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.

[operator@vault ~]$ sudo fail2ban-client set vaultwarden{-admin} unbanip <IP>

Setup logs rotation

[operator@vault ~]$ 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.

[operator@vault ~]$ sudo logrotate -d /etc/logrotate.d/vaultwarden

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