Hosting Bitwarden Behind a Reverse Proxy Server

If you’d just like to dive into the code for adding a Bitwarden password manager to the same server hosting our simple WordPress app you can skip ahead to Putting It All Together.

This is Part 9b in a series describing a project to create a local WordPress development environment using Docker-Compose. The series is structured as follows:

In Part 9a we installed the Bitwarden password manager and configured it for our project. You must complete the setup discussed in that article before continuing here. In this article we will configure the nginx service of our WordPress app to act as a reverse proxy for the Bitwarden app. This will closely mirror the work we did when adding multiple WordPress services in Part 8 so I’m not going to dwell overly long on the changes we need to make. Refer back to Part 8 if you need a refresher.

The first step is to add our external Docker network, wp-bw-net, to our nginx service in our Compose file. Next, we need to create a signed SSL certificate for the Bitwarden service that we will reference in our nginx configuration file.

Compose File Changes

Similar to out Compose modifications in Part 9a for the Bitwarden app, we have to add our external Docker network, wp-bw-net, to our WordPress app’s nginx service so the two apps can communicate with each other. However, instead of making this change with an override file as we did in Part 9a, let’s just change the WordPress app Compose file directly. This is appropriate because we don’t have to worry about the file being overwritten.

First add the top-level networks key to your Compose file. Specify our external network the same way we did in Part 9a as follows.

networks:
  wp-bw-net:
    external: true

Next, add the networks key below to our nginx service to allow it to join the external network. Note that I’ve also included the default network. The default network is created by Compose on project startup to facilitate internal communications between the containers in your Compose file, which join the network automatically. If you specify another network for a container, you have to also specify the default network if want that container to join it.

    networks:
      - default
      - wp-bw-net

Create a Signed SLL Certificate for Bitwarden

As with our existing WordPress sites, we’ll access Bitwarden over a secure network connection with https protocol. As such we’ll need to create an SSL certificate for it.

I covered how to create a signed SSL certificate in Part 7a – Creating Your Own Certificate Authority. If you’ve completed that tutorial it is a simple matter to create a signed SSL certificate for Bitwarden. Simply run the create-cert.sh script, as discussed in Part 7, in the /part-9/secrets/certs folder for the bw.local domain that we specified in Part 9a when installing Bitwarden.

To map this domain to your server’s IP address, you’ll need to add the bw.local domain to your hosts file as described in Part 7b – Securing Network Communications with Your Own Certificate Authority. To do this you can simply duplicate the WordPress domain line in your hosts file and substitute the bw.local domain for the wordpress domain.

NGINX Configuration File Changes

Last up, we need to configure our web server for Bitwarden. We can accomplish this by simply copying a set of our existing WordPress associated blocks and editing them for Bitwarden.

Starting with our nginx configuration file from Part 8, copy the WordPress server blocks for ports 80 and 443 and create similar blocks for our Bitwarden domain, replacing wordpress with bw.local. Your added server should look like the following for port 80.

server {
  listen 80;
  listen [::]:80;
  server_name bw.local;

  return 301 https://bw.local$request_uri;
}

The server should look like the following for port 443. Note that we’re passing requests to the Bitwarden nginx service over port 8080.

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name bw.local;

  ssl_certificate /etc/nginx/certs/bw.local.crt;
  ssl_certificate_key /etc/nginx/certs/bw.local.key;

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto http;

    proxy_pass http://bitwarden-nginx:8080;
  }
}

And that’s it. Any requests to Bitwarden over http protocol will be automatically redirected to the https server which will pass the requests to Bitwarden on port 8080 which it exposes by default. Docker automatically handles the communications between the apps over the wp-bw-net network we created in Part 9a. Note that communication over the wp-bw-net network uses the http protocol and thus is not encrypted. However, Bitwarden encrypts all sensitive information flowing between its password vault and web browser client so your sensitive data is not at risk. In a future article I’ll show how to encrypt communications over the wp-bw-net network for added security as well.

Putting It All Together

Start with a copy of the project folder from Part 8. I’ve labeled my folder part-9.

Our final Compose file is shown below. Note that I’ve included the depends_on key in the nginx service as discussed at the end of Part 8 to ensure our nginx service starts after those services referenced in its configuration file.

Updated docker-compose.yml file with Bitwarden network

version: '3.7'

services:
  db:
    image: mariadb
    secrets:
      - mysql_root_password
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE_FILE: /run/secrets/mysql_database
      MYSQL_USER_FILE: /run/secrets/mysql_user
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - db:/var/lib/mysql
      - ./secrets/init:/docker-entrypoint-initdb.d

  wp:
    image: wordpress
    secrets:
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - wp:/var/www/html

  wp2:
    image: wordpress
    secrets:
      - mysql_database2
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database2
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - wp2:/var/www/html

  wp3:
    image: wordpress
    secrets:
      - mysql_database3
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database3
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - wp3:/var/www/html

  adminer:
    image: adminer

  nginx:
    image: nginx
    depends_on:
      - wp
      - wp2
      - wp3
      - adminer
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./secrets/certs:/etc/nginx/certs
    networks:
      - default
      - wp-bw-net

volumes:
  db:
  wp:      
  wp2:      
  wp3:      

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password
  mysql_database:
    file: ./secrets/mysql_database
  mysql_database2:
    file: ./secrets/mysql_database2
  mysql_database3:
    file: ./secrets/mysql_database3
  mysql_user:
    file: ./secrets/mysql_user
  mysql_password:
    file: ./secrets/mysql_password

networks:
  wp-bw-net:
    external: true

Our final nginx configuration file is as follows.

Updated nginx/nginx.conf file configured for Bitwarden

server {
  listen 80;
  listen [::]:80;

  server_name wordpress;

  return 301 https://wordpress$request_uri;
}

server {
  listen 80;
  listen [::]:80;

  server_name wordpress2;

  return 301 https://wordpress2$request_uri;
}

server {
  listen 80;
  listen [::]:80;

  server_name wordpress3;

  return 301 https://wordpress3$request_uri;
}

server {
  listen 80;
  listen [::]:80;

  server_name adminer;

  return 301 https://adminer$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name wordpress;

  ssl_certificate /etc/nginx/certs/wordpress.crt;
  ssl_certificate_key /etc/nginx/certs/wordpress.key;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;

  location / {
    proxy_pass http://wp;
  }

}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name wordpress2;

  ssl_certificate /etc/nginx/certs/wordpress2.crt;
  ssl_certificate_key /etc/nginx/certs/wordpress2.key;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;

  location / {
    proxy_pass http://wp2;
  }

}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name wordpress3;

  ssl_certificate /etc/nginx/certs/wordpress3.crt;
  ssl_certificate_key /etc/nginx/certs/wordpress3.key;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;

  location / {
    proxy_pass http://wp3;
  }

}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name adminer;

  ssl_certificate /etc/nginx/certs/adminer.crt;
  ssl_certificate_key /etc/nginx/certs/adminer.key;

  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto https;

  location / {
    proxy_pass http://adminer:8080;
  }

}

server {
  listen 80;
  listen [::]:80;
  server_name bw.local;

  return 301 https://bw.local$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name bw.local;

  ssl_certificate /etc/nginx/certs/bw.local.crt;
  ssl_certificate_key /etc/nginx/certs/bw.local.key;

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto http;

    proxy_pass http://bitwarden-nginx:8080;
  }
}

Finally, make sure you’ve installed and configured Bitwarden and created the wp-bw-net network as described in Part 9a. Also make sure you’ve created the signed SSL certificate and edited your hosts file for Bitwarden as discussed in Create Signed Certificate for Bitwarden above. Assuming you’ve created and installed your own Certificate Authority per Part 7a – Creating Your Own Certificate Authority, then your all set to go as we haven’t made changes to any other files used in Part 8.

You can now start up Bitwarden and your WordPress app. We have to start up Bitwarden first as our WordPress app’s nginx service references it. As such Bitwarden must be up and running or the nginx service in the WordPress app will fail to start. Start Bitwarden by running its master script with the start option.

./bitwarden.sh start

After Bitwarden has started you can start up your WordPress app with docker-compose up -d. Going forward, you might want to make a script to perform both of these commands in one go.

Let’s take a look at the containers we’ve created with the docker ps -a command. Recall that we use the -a option to list any containers that may have failed to start.

CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS                   PORTS                                      NAMES
d2fec9fe7b01        nginx                            "nginx -g 'daemon of…"   6 minutes ago       Up 6 minutes             0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   part-9_nginx_1
920365539c11        wordpress                        "docker-entrypoint.s…"   6 minutes ago       Up 6 minutes             80/tcp                                     part-9_wp3_1
42c0d839df27        wordpress                        "docker-entrypoint.s…"   6 minutes ago       Up 6 minutes             80/tcp                                     part-9_wp2_1
f2f10569ab15        wordpress                        "docker-entrypoint.s…"   6 minutes ago       Up 6 minutes             80/tcp                                     part-9_wp_1
b99ff851188d        adminer                          "entrypoint.sh docke…"   6 minutes ago       Up 6 minutes             8080/tcp                                   part-9_adminer_1
82bce331051b        mariadb                          "docker-entrypoint.s…"   6 minutes ago       Up 6 minutes             3306/tcp                                   part-9_db_1
fd1520a5374d        bitwarden/nginx:1.33.1           "/entrypoint.sh"         8 minutes ago       Up 8 minutes (healthy)   80/tcp, 8080/tcp, 8443/tcp                 bitwarden-nginx
0f0f6a4025c2        bitwarden/admin:1.33.1           "/entrypoint.sh"         9 minutes ago       Up 8 minutes (healthy)   5000/tcp                                   bitwarden-admin
99fbe6fc04c4        bitwarden/icons:1.33.1           "/entrypoint.sh"         9 minutes ago       Up 8 minutes (healthy)   5000/tcp                                   bitwarden-icons
6d586f7baa17        bitwarden/events:1.33.1          "/entrypoint.sh"         9 minutes ago       Up 8 minutes (healthy)   5000/tcp                                   bitwarden-events
62fdf6104b0d        bitwarden/api:1.33.1             "/entrypoint.sh"         9 minutes ago       Up 8 minutes (healthy)   5000/tcp                                   bitwarden-api
b3502e061dac        bitwarden/identity:1.33.1        "/entrypoint.sh"         9 minutes ago       Up 8 minutes (healthy)   5000/tcp                                   bitwarden-identity
88cec8d65e7f        bitwarden/notifications:1.33.1   "/entrypoint.sh"         9 minutes ago       Up 9 minutes (healthy)   5000/tcp                                   bitwarden-notifications
00aaabece99b        bitwarden/attachments:1.33.1     "/entrypoint.sh"         9 minutes ago       Up 9 minutes (healthy)                                              bitwarden-attachments
cbb68d95148d        bitwarden/mssql:1.33.1           "/entrypoint.sh"         9 minutes ago       Up 9 minutes (healthy)                                              bitwarden-mssql
660e25041de1        bitwarden/web:2.13.2             "/entrypoint.sh"         9 minutes ago       Up 9 minutes (healthy)                                              bitwarden-web

You can see that along with our WordPress associated containers, we’ve created ten additional containers associated with the Bitwarden service. Of particular interest is the bitwarden-nginx container. You can see that it is configured to expose ports 80, 8080 and 8443 for communications with other containers on the same network.

You can access Bitwarden in your browser with https://bw.local. If you access it with the http protocol, Nginx will redirect your request to a https connection. If you’ve properly created the SSL certificate for this domain then you should be taken directly to the Bitwarden web client login page.

Bitwarden login page

To begin, click Create Account and fill in the requested account information on the Create Account page. Note that at the time of this writing, Bitwarden requires a password to be at least eight characters long. When you’ve finished entering your account information, hit Submit to create your new Bitwarden account. You’ll then be taken back to the login page to log into your newly created account. After logging in you’ll be directed to your My Vault page.

Bitwarden My Vault page

At the time of this writing, Bitwarden did not have a Getting Started guide, but its interface is very intuitive. I’m not going to cover using Bitwarden. If you need help you can refer to Bitwarden’s Help Center for a variety of help topics or a third-party article, Bitwarden: How To, for a fairly comprehensive set up guide.

In the upper righthand corner of the My Vault page you’ll see a Verify Email notice which allows you to verify your account email address to unlock all Bitwarden features. I’m not sure which features are unlocked after verifying your email, but it seems like the most commonly used features are available without doing this. Note that if you click on Send Email, you’ll see a notice informing you to check your email for a verification link. However, you won’t receive the email as we haven’t set up Bitwarden’s SMTP mail server settings. I’ll show you how to do this in a future article.

One useful Bitwarden feature you might want to check out is a Bitwarden Browser Extension which allows easy access to your password vault from your favorite browser and allows you to automatically fill-in login page information for sites associated with items in your vault. Bitwarden has extensions for many web browsers on their download page. I’ve tested the extension for Chrome successfully with my self-hosted version of Bitwarden. I was not able to get the Microsoft Edge extension working for self-hosting but it does work for Bitwarden’s cloud vault. Note that the browser extention does work in the new Microsoft Edge browser.

An Alternate Configuration

Before we wrap up, I thought it might be interesting to cover one of the test configurations I created in investigating how to get Bitwarden and WordPress working together. A logical question regarding the structure shown at the beginning of Part 9a is why not just use the Bitwarden nginx service to handle the WordPress app as well. This in fact was the first approach I took. As part of investigating possibilities of hosting both Bitwarden and WordPress on my Ubuntu server, I successfully tested such a set up. At the time I rejected it because I thought the only way to configure this was to modify Bitwarden’s default nginx configuration file for my WordPress app servers. This isn’t desirable since any modifications would be overwritten when Bitwarden was updated. What I didn’t realize at the time was that nginx can use multiple configuration files when starting up and thus I could maintain my WordPress app nginx configuration separate from Bitwarden’s, much like using a Docker Compose override file.

There are other limitations however, that make this solution less than ideal for my project. Bitwarden’s Compose file is set to version 3. As such any override file must also be version 3. You’ll note that our WordPress project specifies version 3.7 and thus would need to be modified to version 3 to be successfully used as a Compose override file with Bitwarden. However, at least at the time of my testing and with my version of Docker and Compose, version 3.1 of the Compose file is required to use secrets, so at the minimum our WordPress app Compose file would need to be modified to eliminate the use of secrets.

Perhaps the biggest drawback of hosting our WordPress app behind Bitwarden is that future expansion of my server would be limited to changes I could make in Bitwarden’s docker-compose.override.yml file. I’d prefer to have this file limited to just the modifications needed to run Bitwarden rather than house all of the Compose apps running on my server. I could make the same argument for my WordPress app, but ultimately, I could just move the WordPress app nginx service to its own app if needed. I don’t have this problem with my current setup. My only limitation is that I have to start all dependent apps prior to my WordPress app, the one containing the main nginx server. Well, enough looking back. I’ll leave it to you to try this setup if you find it interesting or useful for your purposes.

Wrapping Up

You can shut down your Bitwarden app with ./bitwarden.sh stop. This will stop the Bitwarden service but maintain your password vault. You can restart Bitwarden with ./bitwarden.sh start and access your password vault as before. I’ll cover how to backup and restore your password vault in a future article.

As before, you can shut down your WordPress app with docker-compose down while maintaining all of your WordPress site data in Docker volumes. Use the -v option if you’d like to remove these volumes but be careful as you’ll lose all of the data associated with your WordPress app. From experience I know how easy it is to mistakenly include the -v option out of habit when shutting down a Compose project. If you’re likely to make such a mistake, create a script to shut down the services for you.

I’m going to take a break from the tutorial series for a while to cover some information that may be useful to those just starting the series. Check back later for more.