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:
- 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 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:
- 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 Rocky Linux 8.
- Throughout this post, we will access Vaultwarden via its domain name
vault.local
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 Vaultwarden.
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:
- 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
. - 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.
[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:
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 vaultwarden vaultwarden
: 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.
[operator@vault ~]$ sudo logrotate -d /etc/logrotate.d/vaultwarden
Finally, logrotate creates an entry in /etc/cron.daily/logrotate
to execute the configured tasks.