Bitwarden is a password manager that allows users to generate and store strong passwords. It also handles other types of data like secure notes and credit card information. At the time of this writing, it is one of the best password managers out there because in addition to offering strong and zero-knowledge encryption, the codebase is open source. This means that anyone can inspect the code and run it for their personal use. In this article, we’ll go over the steps to build a fully functioning Bitwarden instance that anyone can run on a server at home.
Docker setup
Let’s start by setting up a docker-compose.yml
file to run the Bitwarden server:
version: "3.8"
services:
bitwarden:
container_name: "bitwarden"
image: vaultwarden/server
ports:
- "3012:3012"
volumes:
- data:/data
volumes:
data:
Here, we’re using vaultwarden/server image which is a rust-based version of Bitwarden server. We’re exposing a TCP port to be able to communicate with the container from outside of Docker network. Then, we create a volume and mount it on /data inside the container. That’s where Bitwarden stores its data, including users, vaults, and attachments.
If we bring up this container and try to access Bitwarden through
https://localhost:3012
, it will not work. Bitwarden requires all communications
to go through a TLS tunnel. In other words, we need to use HTTPS instead of
HTTP. We’re going to use a second container running Nginx to handle TLS for us
and proxy requests to Bitwarden’s container.
version: "3.8"
services:
bitwarden:
# ...
depends_on:
- nginx
nginx:
image: nginx:1.21.0-alpine
ports:
- "23984:443"
volumes:
- ./nginx/ssl:/etc/ssl:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
restart: unless-stopped
volumes:
data:
In this configuration, we’re declaring a new service nginx
using the alpine
image of nginx
, exposing a port so that the container can be reacher from
outside Docker’s internal network, and specifying a couple of read-only volumes.
We’re also instructing Docker to automatically restart this container unless it
was manually stopped. This can help increase availability by automatically
recovering from sudden crashes.
The final result looks like follows. Notice that we introduced a environment
variable ADMIN_TOKEN
that can be used to access the admin section of
Bitwarden. More on how to do this later.
version: "3.8"
services:
bitwarden:
container_name: "bitwarden"
image: vaultwarden/server
environment:
- ADMIN_TOKEN=SOME_SECRET_TOKEN
volumes:
- data:/data
depends_on:
- nginx
nginx:
image: nginx:1.21.0-alpine
ports:
- "23984:443"
volumes:
- ./nginx/ssl:/etc/ssl:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
restart: unless-stopped
volumes:
data:
TLS Support
Now that we have our containers set up, it’s time to configure Nginx to handle
TLS connections and forward them to Bitwarden’s container. Let’s create a
server configuration file named bitwarden.conf
and place it under
nginx/conf.d/
. This path relative to wherever you place the
docker-compose.yml
file.
server {
listen 443 ssl http2;
# Allow large attachments
client_max_body_size 128M;
location / {
proxy_pass http://bitwarden:80;
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://bitwarden:3012;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub/negotiate {
proxy_pass http://bitwarden:80;
}
}
This is a pretty typical Nginx proxy configuration where we define a port to listen to along with protocols we want to handle. Next, we have a directive to allow large attachments1 and we declare few paths and how they should be handled. We’re not going to dive into each directive used here but If you want to know more about how to configure Nginx, I encourage to check out their official documentation.
If you’ve been following along you’ll notice that we haven’t addressed the SSL connection yet, and without it our setup will not work. So let’s figure that out. In case you’re planning to use a real domain name for your Bitwarden instance, you can skip this section and jump directly to Nginx Configuration.
In order to make a self-signed TLS certificate, first we need to generate a private key and a certificate for our Bitwarden server using the following command.
OUT_FOLDER=/path/to/ssl
DOMAIN=your.bitwarden.domain
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:$DOMAIN\nbasicConstraints=CA:true")) \
-keyout $OUT_FOLDER/private/nginx-bitwarden.key \
-out $OUT_FOLDER/certs/nginx-bitwarden.cert \
-reqexts SAN -extensions SAN \
-subj "/C=US/ST=New York/L=New York/O=Company Name/OU=Bitwarden/CN=$DOMAIN"
This will generate a new public key nginx-bitwarden.key
using RSA with a key
length of 4096
bits, and a self-signed TLS certificate nginx-bitwarden.cert
valid for 365
days in the private/
and certs/
folders respectively. These
folders should be placed under the folder ssl/
that lives alongside our
docker-compose.yml
file, so make sure you set the $OUT_FOLDER
variable
properly. In addition, the $DOMAIN
should be set to the fully qualified domain
name of the machine where you’re planning to run Bitwarden and Nginx. It’s not
complicated to assign an FQDN to your local machine using a local DNS,
especially if you’re running Pihole. If you’re interested in getting Pihole set
up locally with Docker, check out this article.
Nginx Configuration
Now that we have our certificate and private key, we should tell Nginx where they are located in order to use them for TLS connections.
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/nginx-bitwarden.crt;
ssl_certificate_key /etc/ssl/private/nginx-bitwarden.key;
# ...
}
It’s recommended to generate a Diffie-Hellman file for stronger connections and use it in our Nginx configuration. We can generate one with the command:
openssl dhparam -out $OUT_FOLDER/certs/dhparam.pem 4096
To be honest, I haven’t yet looked at the functions of this Diffie-Hellman file to fully understand how it improves security. But if you know more about it and are interested in contributing your knowledge to this article, feel free to reach out to me on Mastodon.
The complete Nginx configuration after linking the .pem
file should look like
follows.
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/nginx-bitwarden.crt;
ssl_certificate_key /etc/ssl/private/nginx-bitwarden.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
client_max_body_size 128M;
location / {
proxy_pass http://bitwarden:80;
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://bitwarden:3012;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub/negotiate {
proxy_pass http://bitwarden:80;
}
}
Finally, we have our containers configured to run Bitwarden. All we need do to in order to bring them up is run:
docker-compose up -d bitwarden
Once the containers are up, you’ll be able to reach Bitwarden through its domain
name. Given our example configuration, that would be
https://your.bitwarden.domain
. The first time you open the page, your browser
will most likely complain about an insecure connection and display a flashy
warning. If it doesn’t, stop using this browser and get yourself a real one!
This is due to the self-signed certificate. Since the certificate is not signed
by a Certificate Authority (CA) present in the browsers or the OS’s root CA
store, the browser considers the connection insecure. You can add an exception
for this certificate in your browser so that it doesn’t freak out every time you
access your self-hosted Bitwarden. Similarly, you will have to install this
certificate on your device and trust it, otherwise the OS will prevent
Bitwarden’s app to communicate with our running container. To do so, send the
nginx-bitwarden.crt
file we generated in the TLS Support section to your
device2, open it and follow the installation steps. It should be
straightforward in most OS’s but it’s always safe to follow the official guide
to make sure you cover all the steps. For instance, in iOS, on top of installing
the certificate you have to explicitly trust it for TLS connections.
Admin Access
Like we’ve seen in a previous section, in order to unlock the admin panel we
need to set a token in the ADMIN_TOKEN
environment variable. Let’s generate a
token using the following command.
openssl rand -base64 48
Once Bitwarden’s container is restarted after setting the admin token, you can
access the admin panel at https://your.bitwarden.domain/admin
.
Wrap up
We finally made it. We built a docker setup running a container for the Bitwarden server with an Nginx proxy handling TLS connections using a self-signed certificate. This grants us full control over our credentials and any other data we choose to store in Bitwarden, all without having to trust any third party to handle this for us.