Bitwarden: How to setup a self-hosted password manager

Bitwarden is an open-source password manager. Using Bitwarden_rs, it is possible to create a self-hosted server, using little resources, enabling you to use all its features.

Bitwarden makes it possible to share and sync usernames and passwords across all devices and webbrowsers.

In this example I’ll be using two pc’s: One for compiling and one for hosting, because the VPS I run this on isn’t powerful enough to compile the binaries.

On compile machine

Install dependencies

sudo apt update && apt list -u && sudo apt dist-upgrade -y
sudo apt install dirmngr git libssl-dev pkg-config build-essential curl wget libmariadb-dev-compat libmariadb-dev libpq-dev

curl -sL https://deb.nodesource.com/setup_12.x | sudo bash -

sudo apt install nodejs

curl https://sh.rustup.rs -sSf | sh

source $HOME/.cargo/env

Compile bitwarden_rs

git clone https://github.com/dani-garcia/bitwarden_rs.git
cd bitwarden_rs/

git checkout "$(git tag --sort=v:refname | tail -n1)" # checkout most recent version

cargo build --features sqlite,postgresql,mysql --release

cd ..

Compile vault

Clone and checkout repository

git clone https://github.com/bitwarden/web.git
cd web
git checkout "$(git tag --sort=v:refname | tail -n1)" # checkout most recent version

Patch web vault to work with Bitwarden RS

Download the most recent Bitwarden_RS patch for the Bitwarden web vault. This can be done using one of two ways:

A. Download and apply a patch based on the version that you just checked out using git.

wget https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/$(git tag --sort=v:refname | tail -n1).patch
git apply $(git tag --sort=v:refname | tail -n1).patch

B. Does this give a 404 Not Found error? In that case there might not be new changes in the most recent Bitwarden web release that need to be patched in order to work with Bitwarden RS.

For example:

wget https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/v2.17.1.patch
git apply v2.17.1.patch

Build the web vault

NB: Do not run the following commands as root. Building the web vault will fail.

npm run sub:init
npm install
npm audit fix
npm run dist

cd ..

Copy

export SSH="user@host.example.org"

scp -r bitwarden_rs/target/release/bitwarden_rs ${SSH}:~

scp bitwarden_rs/.env.template ${SSH}:~

scp -r web/build ${SSH}:~/web-vault

on remote host

sudo adduser --system --group --disabled-login --home /opt/bitwarden bitwarden

sudo cp -r ~/bitwarden_rs /opt/bitwarden
sudo cp -r ~/web-vault /opt/bitwarden/

rm ~/bitwarden_rs
rm -rf ~/web-vault

sudo mkdir /etc/bitwarden
sudo nano /etc/bitwarden/bitwarden.conf

Use this file as a template. Alter all uncommented variables to match your environment.

## Bitwarden_RS Configuration File
## Uncomment any of the following lines to change the defaults
##
## Be aware that most of these settings will be overridden if they were changed.
## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .

## Main data folder
# DATA_FOLDER=data

## Database URL
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
# DATABASE_URL=data/db.sqlite3
## When using MySQL, specify an appropriate connection URI.
## Details: https://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html
# DATABASE_URL=mysql://user:password@host[:port]/database_name
## When using PostgreSQL, specify an appropriate connection URI (recommended)
## or keyword/value connection string.
## Details:
## - https://docs.diesel.rs/diesel/pg/struct.PgConnection.html
## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
# DATABASE_URL=postgresql://user:password@host[:port]/database_name

## Database max connections
## Define the size of the connection pool used for connecting to the database.
# DATABASE_MAX_CONNS=10

## Individual folders, these override %DATA_FOLDER%
# RSA_KEY_FILENAME=data/rsa_key
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments

## Templates data folder, by default uses embedded templates
## Check source code to see the format
# TEMPLATES_FOLDER=/path/to/templates
## Automatically reload the templates for every request, slow, use only for development
# RELOAD_TEMPLATES=false

## Client IP Header, used to identify the IP of the client, defaults to "X-Client-IP"
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
# IP_HEADER=X-Client-IP

## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
# ICON_CACHE_TTL=2592000
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
# ICON_CACHE_NEGTTL=259200

## Web vault settings
# WEB_VAULT_FOLDER=web-vault/
# WEB_VAULT_ENABLED=true

## Enables websocket notifications
WEBSOCKET_ENABLED=true

## Controls the WebSocket server address and port
WEBSOCKET_ADDRESS=127.0.0.1
# WEBSOCKET_PORT=3012

## Enable extended logging, which shows timestamps and targets in the logs
# EXTENDED_LOGGING=true

## Timestamp format used in extended logging.
## Format specifiers: https://docs.rs/chrono/latest/chrono/format/strftime
# LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f"

## Logging to file
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
LOG_FILE=/var/log/bitwarden/bitwarden.log

## Logging to Syslog
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# USE_SYSLOG=false

## Log level
## Change the verbosity of the log output
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
## Setting it to "trace" or "debug" would also show logs for mounted
## routes and static file, websocket and alive requests
# LOG_LEVEL=Info

## Enable WAL for the DB
## Set to false to avoid enabling WAL during startup.
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
## this setting only prevents bitwarden_rs from automatically enabling it on start.
## Please read project wiki page about this setting first before changing the value as it can
## cause performance degradation or might render  the service unable to start.
# ENABLE_DB_WAL=true

## Database connection retries
## Number of times to retry the database connection during startup, with 1 second delay between each retry, set to 0 to retry indefinitely
# DB_CONNECTION_RETRIES=15

## Disable icon downloading
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
## otherwise it will delete them and they won't be downloaded again.
# DISABLE_ICON_DOWNLOAD=false

## Icon download timeout
## Configure the timeout value when downloading the favicons.
## The default is 10 seconds, but this could be to low on slower network connections
# ICON_DOWNLOAD_TIMEOUT=10

## Icon blacklist Regex
## Any domains or IPs that match this regex won't be fetched by the icon service.
## Useful to hide other servers in the local network. Check the WIKI for more details
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^

## Any IP which is not defined as a global IP will be blacklisted.
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
# ICON_BLACKLIST_NON_GLOBAL_IPS=true

## Disable 2FA remember
## Enabling this would force the users to use a second factor to login every time.
## Note that the checkbox would still be present, but ignored.
# DISABLE_2FA_REMEMBER=false

## Maximum attempts before an email token is reset and a new email will need to be sent.
# EMAIL_ATTEMPTS_LIMIT=3

## Token expiration time
## Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
# EMAIL_EXPIRATION_TIME=600

## Email token size
## Number of digits in an email token (min: 6, max: 19).
## Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting!
# EMAIL_TOKEN_SIZE=6

## Controls if new users can register
# SIGNUPS_ALLOWED=true

## Controls if new users need to verify their email address upon registration
## Note that setting this option to true prevents logins until the email address has been verified!
## The welcome email will include a verification link, and login attempts will periodically
## trigger another verification email to be sent.
# SIGNUPS_VERIFY=false

## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time
## an email verification link has been sent another verification email will be sent
# SIGNUPS_VERIFY_RESEND_TIME=3600

## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification
## email will be re-sent upon an attempted login.
# SIGNUPS_VERIFY_RESEND_LIMIT=6

## Controls if new users from a list of comma-separated domains can register
## even if SIGNUPS_ALLOWED is set to false
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org

## Controls which users can create new orgs.
## Blank or 'all' means all users can create orgs (this is the default):
# ORG_CREATION_USERS=
## 'none' means no users can create orgs:
# ORG_CREATION_USERS=none
## A comma-separated list means only those users can create orgs:
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com

## Token for the admin interface, preferably use a long random string
## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled
ADMIN_TOKEN=____PASSWORD____

## Enable this to bypass the admin panel security. This option is only
## meant to be used with the use of a separate auth layer in front
# DISABLE_ADMIN_TOKEN=false

## Invitations org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true
## Name shown in the invitation emails that don't come from a specific organization
# INVITATION_ORG_NAME=Bitwarden_RS

## Per-organization attachment limit (KB)
## Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
# ORG_ATTACHMENT_LIMIT=
## Per-user attachment limit (KB).
## Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more
# USER_ATTACHMENT_LIMIT=

## Controls the PBBKDF password iterations to apply on the server
## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000

## Whether password hint should be sent into the error response when the client request it
SHOW_PASSWORD_HINT=false

## Domain settings
## The domain must match the address from where you access the server
## It's recommended to configure this value, otherwise certain functionality might not work,
## like attachment downloads, email links and U2F.
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
DOMAIN=https://vault.example.org

## Allowed iframe ancestors (Know the risks!)
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
## Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
## This adds the configured value to the 'Content-Security-Policy' headers 'frame-ancestors' value.
## Multiple values must be separated with a whitespace.
# ALLOWED_IFRAME_ANCESTORS=

## Yubico (Yubikey) Settings
## Set your Client ID and Secret Key for Yubikey OTP
## You can generate it here: https://upgrade.yubico.com/getapikey/
## You can optionally specify a custom OTP server
# YUBICO_CLIENT_ID=11111
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify

## Duo Settings
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
## Then set the following options, based on the values obtained from the last step:
# DUO_IKEY=<Integration Key>
# DUO_SKEY=<Secret Key>
# DUO_HOST=<API Hostname>
## After that, you should be able to follow the rest of the guide linked above,
## ignoring the fields that ask for the values that you already configured beforehand.

## Authenticator Settings
## Disable authenticator time drifted codes to be valid.
## TOTP codes of the previous and next 30 seconds will be invalid
##
## According to the RFC6238 (https://tools.ietf.org/html/rfc6238),
## we allow by default the TOTP code which was valid one step back and one in the future.
## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes.
## You can disable this, so that only the current TOTP Code is allowed.
## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid.
## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid.
# AUTHENTICATOR_DISABLE_TIME_DRIFT = false

## Rocket specific settings, check Rocket documentation to learn more
# ROCKET_ENV=staging
ROCKET_ADDRESS=127.0.0.1
# ROCKET_PORT=8000
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}

## Mail specific settings, set SMTP_HOST and SMTP_FROM to enable the mail service.
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
SMTP_HOST=smtp.example.org
SMTP_FROM=vault@example.org
# SMTP_FROM_NAME=Bitwarden_RS
SMTP_PORT=587
SMTP_SSL=true
# NB: (Explicit) - SMTP_SSL by default configures Explicit STARTTLS, it will upgrade an insecure connection to a secure one. Unless SMTP_EXPLICIT_TLS is set to true.
# SMTP_EXPLICIT_TLS=true
# NB: (Implicit) SMTP_EXPLICIT_TLS configures Implicit TLS. It's currently mislabelled (see bug #851) - SMTP_SSL Needs to be set to true for this option to work.
# SMTP_USERNAME=username
SMTP_USERNAME=vault@example.org
SMTP_PASSWORD=____PASSWORD____
# SMTP_TIMEOUT=15
## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections.
## Possible values: ["Plain", "Login", "Xoauth2"].
## Multiple options need to be separated by a comma ','.
# SMTP_AUTH_MECHANISM="Plain"

## Server name sent during the SMTP HELO
## By default this value should be is on the machine's hostname,
## but might need to be changed in case it trips some anti-spam filters
# HELO_NAME=

## Require new device emails. When a user logs in an email is required to be sent.
## If sending the email fails the login attempt will fail!!
# REQUIRE_DEVICE_EMAIL=false

## HIBP Api Key
## HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
# HIBP_API_KEY=

# vim: syntax=ini
sudo chmod 600 /etc/bitwarden/bitwarden.conf
sudo chown bitwarden:bitwarden /etc/bitwarden/bitwarden.conf

sudo mkdir /opt/bitwarden/data
sudo mkdir /opt/bitwarden/data/db-backup

sudo chown -R bitwarden:bitwarden /opt/bitwarden

sudo mkdir /var/log/bitwarden
sudo chown -R bitwarden:bitwarden /var/log/bitwarden

Add nginx vault.conf. The following config assumes that you have already installed and configured certbot / letsencrypt and retrieved a certificate.

sudo nano /etc/nginx/conf.d/vault.conf
server {
    listen 80;
    server_name vault.example.org;

    location /.well-known/acme-challenge/ {
        root /var/www/acme;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name vault.example.org;

    client_max_body_size 128M;

    ssl_certificate /etc/letsencrypt/live/vault.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.org/privkey.pem;
    ssl_dhparam /etc/ssl/certs/dhparam_4096.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "ECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 127.0.0.1 valid=300s;
    resolver_timeout 5s;
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security "max-age=63072000; preload";
    keepalive_timeout 300s;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /notifications/hub {
        proxy_pass http://127.0.0.1:3012;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /notifications/hub/negotiate {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:8000;
    }
}
sudo nginx -t
sudo systemctl restart nginx
sudo nano /etc/systemd/system/bitwarden.service
[Unit]
Description=Bitwarden server
Documentation=https://github.com/dani-garcia/bitwarden_rs
After=network.target auditd.service

# If using an alternative DB backend, uncomment one of the following additional "After" and "Require" sections.
## PostgreSQL
# After=postgresql.service
# Requires=postgresql.service
## MariaDB
# After=mariadb.service
# Requires=mariadb.service
## MySQL
# After=mysqld.service
# Requires=mysqld.service

[Service]
RestartSec=2s
Type=simple

User=bitwarden
Group=bitwarden

EnvironmentFile=/etc/bitwarden/bitwarden.conf

WorkingDirectory=/opt/bitwarden/
ExecStart=/opt/bitwarden/bitwarden_rs
Restart=always

# Isolate bitwarden_rs from the rest of the system
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
NoNewPrivileges=true
ProtectSystem=strict

# Only allow writes to the following directories
ReadWritePaths=/opt/bitwarden/data/ /var/log/bitwarden

# Set reasonable connection and process limits
LimitNOFILE=1048576
LimitNPROC=64

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable bitwarden
sudo systemctl start bitwarden

Set up Fail2ban

If you are using Fail2Ban, you can add this configuration to keep out unwanted guests:

sudo apt install -y fail2ban
sudo nano /etc/fail2ban/filter.d/bitwarden.conf
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <HOST>\. Username:.*$
ignoreregex =
sudo nano /etc/fail2ban/jail.d/bitwarden.local
[bitwarden]
enabled = true
port = 80,443,8081
filter = bitwarden
action = iptables-allports[name=bitwarden]
logpath = /var/log/bitwarden/bitwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
sudo nano /etc/fail2ban/filter.d/bitwarden-admin.conf
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Unauthorized Error: Invalid admin token\. IP: <HOST>.*$
ignoreregex =
sudo nano /etc/fail2ban/jail.d/bitwarden-admin.local
[bitwarden-admin]
enabled = true
port = 80,443
filter = bitwarden-admin
action = iptables-allports[name=bitwarden]
logpath = /var/log/bitwarden/bitwarden.log
maxretry = 5
bantime = 14400
findtime = 14400
sudo systemctl restart fail2ban

Set up logrotation

Over time, the Bitwarden_RS log file can grow to a significant size. Using logrotate, we can periodically rotate logs.

sudo nano /etc/logrotate.d/bitwarden
/var/log/bitwarden/*.log {
    # Perform logrotation as the bitwarden user and group
    su bitwarden bitwarden
    # Rotate daily
    daily
    # Rotate when the size is bigger than 5MB
    size 5M
    # Compress old log files
    compress
    # Keep 4 rotations of log files before removing or mailing to the address specified in a mail directive
    rotate 4
    # Truncate the original log file in place after creating a copy
    copytruncate
    # Don't panic if not found
    missingok
    # Don't rotate log if file is empty
    notifempty
    # Add date instaed of number to rotated log file
    dateext
    # Date format of dateext
    dateformat -%Y-%m-%d-%s
}

NB: To view a compressed log file without manually decompressing:

zcat logfile.gz
zless logfile.gz
zgrep -i keyword_search logfile.gz

Backup

If you’d like to backup the bitwarden server, please use the following steps to do so

sudo apt install -y sqlite3

Export the sqlite database:

sqlite3 /opt/bitwarden/data/db.sqlite3 ".backup '/opt/bitwarden/data/db-backup/backup.sqlite3'"

If you have already set up backups for other services, add these paths to your list of backup targets:

/opt/bitwarden/data/db-backup
/opt/bitwarden/data/attachments
/opt/bitwarden/data/rsa_key.der
/opt/bitwarden/data/rsa_key.pem
/opt/bitwarden/data/rsa_key.pub.der
/opt/bitwarden/data/icon_cache

Upgrade Bitwarden RS and web vault

On build machine

Update system packages

sudo apt update
sudo apt dist-upgrade -y

Remove old build and sources

rm -rf bitwarden_rs web

Upgrade Rust

curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env

Build Bitwarden RS

git clone https://github.com/dani-garcia/bitwarden_rs.git
cd bitwarden_rs/

git checkout "$(git tag --sort=v:refname | tail -n1)" # checkout most recent version

cargo build --features sqlite,postgresql,mysql --release

cd ..

Build Bitwarden web vault

Clone Bitwarden web vault repository
git clone https://github.com/bitwarden/web.git
cd web
git checkout "$(git tag --sort=v:refname | tail -n1)" # checkout most recent version
Download the most recent Bitwarden_RS patch for the Bitwarden web vault

This can be done using one of two ways:

A. Download and apply a patch based on the version that you just checked out using git.

wget https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/$(git tag --sort=v:refname | tail -n1).patch
git apply $(git tag --sort=v:refname | tail -n1).patch

B. Does this give a 404 Not Found error? In that case there might not be new changes in the most recent Bitwarden web release that need to be patched in order to work with Bitwarden RS.

For example:

wget https://raw.githubusercontent.com/dani-garcia/bw_web_builds/master/patches/v2.17.1.patch
git apply v2.17.1.patch
Compile web vault
npm run sub:init
npm install
npm audit fix
npm run dist

cd ..

Copy upgraded binary and vault

export SSH="user@host.example.org"

scp -r bitwarden_rs/target/release/bitwarden_rs ${SSH}:~

scp -r web/build ${SSH}:~/web-vault

On remote / public machine

sudo systemctl stop bitwarden.service
sudo cp -r ~/bitwarden_rs /opt/bitwarden

sudo rm -rf /opt/bitwarden/web-vault
sudo cp -r ~/web-vault /opt/bitwarden/

rm ~/bitwarden_rs
rm -rf ~/web-vault

sudo chown -R bitwarden:bitwarden /opt/bitwarden

sudo systemctl start bitwarden.service
systemctl status bitwarden.service

Update notes

  • 2020-07: Improved web-vault instructions, added upgrade steps.
  • 2020-08: Noticed missing nginx config example. Added this.
  • 2020-09: Added some cleanup steps.
  • 2020-10: Updated to latest versions. Added new configuration options. Include MySQL and PostgreSQL backends next to the existing SQLite backend in compile steps.
  • 2020-12: Update to latest versions. Added logrotate config.

Related