The Bitwarden Password Manager - Host Your Own Server

Categories: Security, Linux

Overview

This article looks at the Bitwarden online password manager - why it is useful, and how to run it on your own server infrastructure as an alternative to using a hosted service.

Important: if you do install Bitwarden locally, then please make a donation to the original developers - the developer 8bit Solutions is a very small (possible one-person) company. If you are not paying for a web-based subscription, then a contribution is appropriate.

And note that if you want to self-host a bitwarden server, the “organisations” feature doesn’t work ie you cannot share passwords with other users (family or team). A license is needed for this (self-hosted system still connects to bitwarden.com for this feature). This feature was free in earlier versions of the Bitwarden open-source release, but no longer.

The Problem

We all need to log on to multiple sites around the internet.

For a few people, “single sign on” with a well-known organisation works; eg once you have a google login, many other sites offer “log in via google” option. However that provides a lot of undesirable information to your central credential provider. And not all sites support such integration.

The alternative is to manage multiple passwords, as reusing the same password in multiple places is a bad idea. However those passwords soon get difficult to keep track of - the best solution is to have some central database of passwords somewhere.

Most web browsers offer a password database, and provide nice integration with websites - autofilling login fields, saving passwords when we create new accounts somewhere, and updating the password database entries when we change passwords via HTML forms. However that only works when using that same browser on that same machine - and needs to be backed up. Browsers have started to offer “password sync” so that the same passwords are both backed up and available from the same browser on other machines - but:

  • you are required to sign up for the sync service
  • using a remote service might provide undesirable info to the provider of that service, depending on the implementation (Mozilla’s “firefox sync” is currently ok, not sure about others)
  • it is not portable across browsers
  • doesn’t help with credentials for things other than web-sites
  • and raises the risk of all your passwords being stolen at once (again, dependent on implementation)

Browser-based password storage is also vulnerable to security bugs - browsers are complex applications with a poor history of security holes.

An alternative to letting the browser store passwords is to install a password manager application locally on your machine. This makes credentials available for more than websites, but:

  • is less convenient to use (need to regularly copy-and-paste between browser and password manager app)
  • is not cross-machine
  • and needs to be backed up

At least these dedicated apps are more secure than browsers, being much simpler and less exposed to the internet.

Various “hosted password repos” exist (lastpass etc) which allow credentials to be stored “in the cloud”. They get backed up, are available on any machine and not just from browsers. Typically these providers also provide browser-plugins to make saving/updating credentials easy. However this puts a lot of trust in the repo provider. These services are also subscription-based.

Bitwarden is one of these “hosted password repos”, but one with several advantages that are discussed in the next section.

About Bitwarden

Bitwarden is a service for hosting credentials, similar to the “hosted password repos” described above, but with a difference:

  • it is completely open-source (both client and server side)
  • the protocol is documented (at least indirectly; people have read the code and written documentation for it)
  • the protocol ensures the server never sees unencrypted credentials (client code does encryption/decryption)

The Bitwarden site is very professional, as are the (wide) selection of available client software. The company backing it may be small, but their work seems very good.

The Bitwarden code can be divided into two parts:

  • server-side
  • client-side

The server-side code consists of a single application, providing a REST api for reading/writing credentials.

The client-side code can be divided into:

  • native graphical and commandline desktop apps for various OSes (linux, mac, windows)
  • cross-platform commandline app
  • web-based desktop app (known as “vault”)
  • mobile apps
  • browser plugins

The desktop apps allow users to configure their “account”, save/search/view credentials, and various other options. These apps make REST calls to the server component to actually read/write credentials. These apps are available in both graphical and commandline versions.

The cross-platform commandline app is a nodejs-based tool that provides the same functionality as the native desktop apps, but does not offer a GUI. As with all other desktop apps, it performs encryption/decryption locally and makes calls to the central REST server to persist data.

The web-based desktop app is a pure javascript “single page app” that provides the same functionality as the native GUI desktop apps, and also makes REST calls to the server component in the same way. The javascript, html, etc. is served by the server-side component (acting as a simple HTTP server of static files). Note that there is no logic on the web-server-side; the server simply provides the javascript files which are then executed client-side within the user’s browser.

Browser plugins provide “auto-fill” and “auto-save” integration by calling the REST api of the Bitwarden server (whichever address and port is configured). The Firefox browser plugin is cross-platform; not sure about plugins for other browsers.

The official server-side rest server from bitwarden.com is implemented in C# and relies on Microsoft-SQLServer as a database. The Bitwarden developers have (somehow) created docker containers which wrap these and can be executed on Linux. However these containers are very resource-intensive. The original developers probably do not care about resources - they use these containers to run their bitwarden.com hosting operation for over 100 thousand customers - and thus requiring a large database is not a significant problem. They kindly offer the containers for others to use - but don’t care that this approach is not convenient for people wanting to host an instance for a small number of users (possibly one).

Fortunately, various people have implemented alternative less-resource-intensive versions of the server-side rest component, rubywarden, bitwarden-go and bitwarden-rs. The bitwarden-rs (Rust-based) implementation seems to be the most active and the least resource-intensive. The remainder of this article looks at how to install this on Linux.

Note: the “team” and “enterprise” accounts offered for the official hosted service have a few extra features (groups, audit-logs, etc). Whether these are available in the self-hosted variant depends on how complete the “back end” implementation is. The bitwarden_rs implementation (discussed later) appears to implement all of the commercial features.

Using the Hosted Service

Before looking at installing a Bitwarden server, it is worth looking at the hosted offering available from bitwarden.com. They offer truly free accounts for single users - with all necessary features. And what is special about Bitwarden is that all data is encrypted before being sent to the hosted service - and thus you do not need to worry about hacks of the central service, or even dishonest sysadmins, as long as:

  • you use a reasonable master password
  • and the client app you are using has not been modified

Paid plans are available for “family” and “business” use. The most important feature of these plans is the ability to share a password across multiple accounts - updating it from one account will update it in all accounts, even though the central service does not know the master passwords of any account.

The prices are extremely reasonable. Currently:

  • personal account, including sharing with 1 other user: free
  • family account, with up to 5 users: $1/month
  • team account, with up to 5 users: $5/month (per account, not per user!). Extra users: $2/month.
  • enterprise account: $3/user/month.

Despite the good security design of Bitwarden, I still like the idea of hosting my passwords myself - so on to the rest of the article. To repeat the warning from the introduction section: the “organisations” feature needed to share passwords between users (family or team) is not available in the open-source code. The open-source code communicates with bitwarden.com for this feature and requires a license.

About the Bitwarden-rs alternative implementation

As noted above, the standard back-end (rest server) implementation uses c# and sqlserver, and is distributed as a set of docker containers that have relatively high resource requirements. This is not very friendly for self-hosting, at least for small-scale use.

The bitwarden_rs project reimplements the back-end in a simpler manner. Bitwarden_rs can use an SQLite embedded database, ie no separate “database server” process is needed and data is stored in a single file on the server local filesystem. Bitwarden-rs is a single process that listens for HTTP requests on a single port, responding to REST api URLs. It can also act as a simple static HTTP web server in order to serve the static files that make up the “vault” web-based client application, if configured to do so.

According to its author, bitwarden_rs requires only 10MB of ram.

Requirements

The Bitwarden server should be accessible only over https; this is particularly important when serving the “vault” web client app, as any attacker who can modify that code effectively has access to passwords.

bitwarden_rs may be configured directly with appropriate HTTPS certificates, or it may be configured to listen on a local port with a reverse-proxy-server on the same machine terminating https requests and forwarding them as http to bitwarden_rs. I personally use nginx already, and so use it as a front-end to Bitwarden.

When using https, a certificate is required. One can be obtained for free via “letsencrypt”. HTTPS also requires that the host has a suitable DNS name.

Bitwarden_rs should of course be started via systemd, and run using a dedicated user account.

Installing via Docker

There is a Docker image for the official Bitwarden version (ie not the alternative rust-based server). However this image is very heavy on resources; among other things it starts an instance of Microsoft SQLServer internally (!). That’s not an option for me, due to resource usage.

When I wrote the first version of this article, bitwarden_rs was only available in source form. The github project now provides prebuilt Docker images which presumably make installation quite easy. See the bitwarden-rs install page for details - a page that did not exist when this article was first written, and which actually does a very good job of documenting the install process.

I’m running a very low-resource (cheap) VPS for my personal use (this website, email, and a few other things). While a container is not too heavy, installing locally is even lighter and this is what is described in the next section.

Installing without Docker

The code is not available as a prebuilt download; it is necessary to build from source. However that is not too hard.

The following build-steps can be performed as any standard user on the host system. Sudo rights will be needed to actually install the results.

Install prerequisites

apt install git
apt install libssl-dev
apt install pkg-config 
apt install gcc

Build Vault Web Client (optional)

As noted earlier, the Vault web client is optional; if all users are content to install native desktop clients or browser-plugins to interact with Bitwarden, then there is no need to build the Vault component. Vault is simply a set of static files that are served up to a user’s browser which then interact with the Bitwarden server via rest calls just like the standard browser-native-plugin interfaces do; Vault has no “back end” components of its own.

Vault is implemented using Microsoft Typescript, and various other technologies which require the “source code” to be processed to generate javascript/html/css/etc. The resulting javascript is also compressed (minimised) in the usual way. There is therefore a “build” process necessary, even though the result is a set of static files which the Bitwarden rest-server simply provides to the user on request. Once loaded into the user’s browser, the code acts just like any other Bitwarden desktop client application.

Although the build process uses npm and nodejs, they are not needed at runtime.

Clone git repository for the Bitwarden “web vault”:

  • git clone https://github.com/bitwarden/web.git
  • cd web
  • git tag
  • git checkout ... # most recent tag (2.16.0 at current date)

Set up build-time tool nodejs (as a normal user, not system-wide):

  • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
  • logout/login to activate the software installed above
  • nvm install --lts

Build:

  • npm run sub:init
  • npm install
  • npm run dist

Build the Rust-based Alternative Server

Install rustc/cargo if you don’t already have it (as a normal user, not system-wide):

  • curl https://sh.rustup.rs -sSf | sh
  • logout/login to activate the software installed above

Clone git repository bitwarden_rs (choose latest tag):

  • git clone https://github.com/dani-garcia/bitwarden_rs.git
  • cd bitwarden_rs
  • git tag
  • git checkout ... # most recent tag

Build:

  • cargo build --release --features sqlite

Install Server and Vault

Rather than run it from where it was built, it seems best to put it somewhere “official”.

# choose user and install-dir
USER=bitwarden
DEST=/var/lib/bitwarden

# create user for bitwarden to run as
adduser --system --no-create-home --disabled-login $USER 

# move required files to $DEST
mkdir $DEST
cp -r target/release/bitwarden_rs $DEST
cp .env.template $DEST/.env
mkdir $DEST/data
cp -r ../web/build $DEST/web-vault   # -- only if you have built Vault earlier, and have enabled it in the .env file
chown -R $USER $DEST

Configure the Server

Configuration of the server can be done in two different ways:

  • via environment variables
  • via data in the database, configured via a commandline tool or the admin web interface

File “.env” in the installation dir created above (copied from env.template in the checkout of bitwarden_rs) defines all the available config options (env-vars). Paths are relative.

See the bitwarden_rs README.md for more information on config options.

Notes:

  • defaults to running on localhost:8000, ie not accessible over internet
  • the server can be configured to listen on an external interface and serve requests directly but the best option is probably to use a reverse proxy to forward to the local daemon, eg nginx.

Edit $DEST/.env and set:

  • update ROCKET (rust http library) settings:
    • set ROCKET_ENV=production
    • when using a reverse-proxy, set ROCKET_ADDRESS=localhost so the server only listens on the internal network interface
    • optionally, update ROCKET_PORT to change the default port on which the server runs
  • set DOMAIN to https://{yourdomain}

Config item ROCKET_TLS is needed only if you are exposing bitwarden_rs directly to the internet; when using a reverse-proxy (eg nginx) the certificate configuration is in the reverse-proxy and not in bitwarden_rs.

Bitwarden provides optional support for web-socket connections from clients to servers. Web socket connections can run in two ways: on a dedicated port, or tunnelled over HTTP. The bitwarden_rs server supports only the dedicated port approach, but a reverse-proxy can be used to tunnel the connections - ie accept a connection on the standard https port then forward to the websocket port on bitwarden_rs. Config options WEBSOCKET_ENABLED, WEBSOCKET_ADDRESS and WEBSOCKET_PORT should be appropriately set in .env` and then the reverse-proxy configured to match; see here for nginx config.

By default config-item SIGNUPS_ALLOWED is enabled, allowing anyone on the internet who can reach the vault site (or the appropriate REST endpoint) to create an account. When this is disabled, users can only create an account when “invited” (sadly the UI is somewhat confusing when account-creation is disabled; everything works until the last step, then fails). If you are willing to purchase a license from bitwarden.com for a Family or Commercial organisation, then you can (as a normal user) create an organisation within a project and any member of that organisation can then invite other users even when SIGNUPS_ALLOWED is disabled. Otherwise, users can be invited by logging into the Vault admin interface (/admin) - assuming bitwarden has been configured with a valid outbound email account. See config items SMTP_* for email configuration.

Configure nginx reverse proxy

This assumes you have used letsencrypt to generate a certificate (eg into /etc/letsencrypt/live/{domain}/fullchain.pem)

cat > /etc/nginx/sites-available/bitwarden.your.domain <<EOF
server {
  # SSL configuration
  listen 8443 ssl http2;
  listen [::]:8443 ssl http2;
  ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
#	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
#	ssl_ciphers HIGH:!aNULL:!MD5;

  server_name your.domain;

  location / {
    proxy_pass http://localhost:8000/;
  }

  # websockets support
  location /notifications/hub {
    proxy_pass http://localhost: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_pass http://localhost:8000;
  }
}
EOF

Running on localhost using http theoretically allows local attackers to make requests to the server, but in practice that seems a very small vulnerability as Bitwarden does not store any unencrypted data - it never has any master keys itself. If you have a different opinion, please let me know in the comments.

For configuration examples for other reverse-proxies, see file PROXY.md in the bitwarden_rs git repo.

As noted in bitwarden_rs documentation, the use of a separate port for websockets is due to current limitations in the Rocket library; this will not be necessary in some future version.

The proxy_set_header commands are primarily to ensure that when bitwarden_rs logs authentication failures, then the IP address it logs is the real external one, and not localhost. This allows fail2ban to be used (see later).

Set up Systemd

Ensure the Bitwarden daemon starts on server boot.

mkdir /etc/bitwarden
cat > /etc/bitwarden/bitwarden.conf <<EOF
SERVER_ADMIN_EMAIL=bwadmin@your.domain
EOF

cat > /etc/systemd/system/bitwarden.service <<EOF
# See https://www.freedesktop.org/software/systemd/man/systemd.unit.html for details
[Unit]
Description=Bitwarden server
After=network.target auditd.service

[Service]
User=bitwarden
Group=nogroup

# todo: use RootDirectory to jail the app
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/bitwarden/data
PrivateTmp=true

EnvironmentFile=/etc/bitwarden/bitwarden.conf
WorkingDirectory=/var/lib/bitwarden 
ExecStart=/var/lib/bitwarden/bitwarden_rs
Restart=on-failure
RestartPreventExitStatus=255
Type=simple

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable bitwarden.service --now
journalctl --unit=bitwarden.service

The SERVER_ADMIN_EMAIL variable defines an address which you can use with “create account” even when configuration has disabled account-creation. That means you should use some Bitwarden client app to create this account (set initial password) immediately after starting the server. This account-id should only be used for administration purposes - and to invite new users when account-creation has been disabled.

If bitwarden logging config (in .env) is still using the defaults (log to STDOUT) then any log output is visible using journalctl --unit bitwarden.

The Admin Portal

In configuration file .env, option ADMIN_TOKEN must be explicitly set to a suitable value to enable the admin interface. After this is set (and bitwarden restarted if needed), URI “/admin” can be used to log in using the TOKEN value specified in the config-file.

The admin-portal can be used to set configuration items, but these are also mostly (all?) settable directly in file .env anyway. More useful is the user-administration tab, and in particular the ability to send invites to users.

Using Fail2Ban

The fail2ban monitoring app can be used to prevent attackers from making guessing attacks on the Vault login page (or bitwarden-rs rest-service login endpoint); login failures are written to a logfile and fail2ban can be set up to scan this logfile, detect the failures, and update the local firewall to (temporarily) block the source IP address from which the login failures came. The temporary point is important in case you accidentally make a few incorrect login attempts yourself!

Fail2ban must be set up to scan whatever file bitwarden_rs has been configured to write to, eg /var/log/syslog or some dedicated file. It can also scan the systemd journal. The exact pattern to be matched depends somewhat on how logging is configured. The current version of fail2ban (included in Ubuntu LTS 20.04) includes a filter-file for bitwarden - but unfortunately it doesn’t match what bitwarden_rs generates. Here’s an alternative file that works for syslog (at the moment anyway).

cat >>/etc/fail2ban/filter.d/bitwarden.conf <<EOF
# Fail2Ban filter for Bitwarden
# Detecting unauthorized access
# Typically logged in /var/log/messages syslog
# Message format to match: 
#   somedate somehost bitwarden_rs[somepid]: [somedate][error][ERROR] Username or password is incorrect. Try again. IP: 1.2.3.4. Username: no@body.

[INCLUDES]
# Read syslog common prefixes
before = common.conf

[DEFAULT]
bwdate = \[.{23}\]

[Definition]
_daemon = bitwarden_rs
failregex = ^%(__prefix_line)s%(bwdate)s\[error\]\[ERROR\] Username or password is incorrect. Try again. IP: <HOST>\. Username: \S+.\s*$
ignoreregex =
EOF

cat >>/etc/fail2ban/jail.d/local.conf <<EOF
[bitwarden]
enabled = true
logpath = /var/log/syslog
EOF

systemctl restart fail2ban
tail -f /var/log/fail2ban.log # and now do some incorrect logins...

Unfortunately, syslog by default converts multiple identical messages occurring within a short time-period into the following form:

  • {prefix}: message repeated 5 times: [ ERROR: Username or password is incorrect. Try again. IP: 1.2.3.4. Username: no@body.]

The only current solution for working with fail2ban is to disable this syslog option:

  • sed -i 's/RepeatedMsgReduction\ on/RepeatedMsgReduction\ off/' /etc/rsyslog.conf
  • systemctl restart rsyslog

The Client/Server Protocol

TODO: write some info on how Bitwarden saves passwords and interacts with clients.

The Browser Plugin

Plugins are available for Firefox, Chrome and Safari. These integrate with the browser to auto-save and auto-fill passwords. When installed, a Bitwarden icon appears next to the nav-bar in the browser; clicking on this gives access to the same features available in the bitwarden desktop client apps. As in the desktop apps, the “gear” icon gives access to the config settings - including setting the path to the bitwarden server.

The built-in password manager in many browsers uses pop-up windows to ask whether to save a new password, etc. - bitwarden instead modifies the HTML to add a prompt at the top of the page. While elegant, I have had problems with one site where the login page keeps refreshing (multiple times per second) until this bar is “dismissed” - I suspect that javascript in this page acts badly when bitwarden changes the page dom.

Notes

When you “log in” to vault (or any other client), you are prompted to provide your “master password”. However actually this password never leaves your browser; the login page uses complex javascript to compute a hash of the master password which verifies your identity but is not sufficient to decrypt any secrets you have stored in bitwarden. This is in effect “log in with password” where the login-pwd can be derived from the master-pwd but not the other way around.

Bitwarden supports both passwords and TOTP codes. In fact, any entry consists of (user-email, encrypted-url) => (set of encrypted properties), where the properties can be anything you need.

There is no need to take particular care when backing up the credentials DB - that is fully encrypted with keys that only the client apps have.

The user interface includes a number of options like “number of iterations for PBKDF”, etc. These options are stored in the central repository so they are available to each client, but are implemented within the client itself.

The Vault web interface is nicely implemented, and elegant to use.

All entries in the repository can be exported if desired, ie there is no “lock in”. Import is supported for the main password manager formats. There is also a “password generator” available.

Sharing of passwords between multiple users is done by defining an “organisation” and then linking multiple users to the same organisation. Each “organisation” effectively has its own vault, and its own copy of the relevant data. Each user who is a “member” of the organisation effectively has access to the “master password” of that organisation. There is a complex structure of rights that an “organisation admin” can configure for users.

The Vault interface for creating organisations lists various free/paid plans. However the bitwarden_rs implementation supports all enterprise features for all organisation types - so when using bitwarden_rs as server, just choose the “free” tier. Fixing this UI quirk would mean forking the Vault code, which is not worth the effort.

There is a command-line tool for interacting with the Bitwarden server; this is implemented in javascript and can be installed with the nodejs package-manager “npm”.

Each item stored has:

  • a “name” (descriptive name for showing in the interface)
  • a “url” (can be searched for)
  • the username for the service to be logged into
  • the password itself, and/or a TOTP code
  • a free-text notes field
  • zero or more “custom fields” associated with the entry
  • a “folder” for organising (grouping) entries

Searches for entries is done client-side, not server-side: the entire repo is “synced” to the local machine and then a search is done locally. Modifications to data include a synchronous update of the central data.

Bitwarden code manages master passwords responsibly; when you provide such a password to any bitwarden app, it immediately derives some other more temporary/limited token from that password and then erases the original master password from memory. In particular, communication with the REST service is done via a “session key” which has a limited time duration; after this has expired the account is considered “locked” and a new session key must be created (by a client app, using the master password). Login is a somewhat different process from a session key - it authenticates against the bitwarden server.

The fact that the rest-server does not actually have access to any credentials means that the fact that bitwarden_rs is not from a well-known source is not a major concern.

Bitwarden server uses WebSockets to notify client applications of changes. The bitwarden_rs server itself provides WebSocket support on a different port than its REST services (3012 by default) - though this different port is not visible if using a reverse_proxy in front (eg the nginx config shown above). This means that client apps connect to bitwarden rest server on the relevant port and stay connected for long periods of time. This functionality is not critical, eg the CLI client does not do this.

Bitwarden’s standard server can interact with LetsEncrypt to allocate certificates automatically; however this requires that it runs on ports 80/443 on its host. Certificate renewal occurs only each time the server is started, ie it is necessary to restart the server at least every 90 days (duration of a letsencrypt certificate). It isn’t clear whether the bitwarden_rs implementation also supports this.

See Also