Skip to main content

Bitwarden with Certbot and Cloudflare DNS

· 5 min read

Bitwarden's standard Linux deployment can automatically generate certificates with Let's Encrypt. It requires that your Bitwarden instance is directly accessible from the internet. This post demonstrates how to use the Cloudflare DNS Certbot plugin that will work for installations behind a firewall.

Install Bitwarden

Follow Bitwarden's deployment instructions. During the ./bitwarden.sh install step, you are prompted to enter some settings. Use these answers:

QuestionAnswer
Do you want to use Let's Encrypt to generate a free SSL certificate?No
Do you have a SSL certificate to use?Yes
Is this a trusted SSL certificate (requires ca.crt, see docs)?Yes

After the installation finishes, do not start Bitwarden yet because we still need a certificate.

Install Certbot

Instead of using Bitwarden's bundled Certbot container, we will install Certbot directly on the host. This gives us the flexibility to use the DNS-01 challenge with supported DNS providers such as Cloudflare, DigitalOcean, and Linode.

Switch to an account with sudo privileges and install Certbot:

sudo apt-get install --yes snapd
sudo snap install --classic certbot
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare
sudo ln -s /snap/bin/certbot /usr/bin/certbot

The Cloudflare DNS Certbot plugin requires a configuration file containing your API key. In this example, it will be stored at /root/cloudflare.ini:

dns_cloudflare_api_token = your_api_token

Set permissions on the file:

sudo chown root:root /root/cloudflare.ini
sudo chmod 600 /root/cloudflare.ini

Request our first certificate

Set the FQDN variable to your server's FQDN. It will be used in subsequent commands:

FQDN=bitwarden.example.com

Run this command to get our first certificate from Let's Encrypt:

sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /root/cloudflare.ini \
--domain $FQDN \
--agree-tos

Copy the certificates over to /opt/bitwarden/bwdata/ssl/ and start Bitwarden:

src=/etc/letsencrypt/live/$FQDN
dest=/opt/bitwarden/bwdata/ssl/$FQDN

sudo cp $src/privkey.pem $dest/private.key
sudo cp $src/fullchain.pem $dest/certificate.crt
sudo cp $src/chain.pem $dest/ca.crt
sudo chown -R bitwarden:bitwarden $dest

sudo su bitwarden -c "cd /opt/bitwarden; ./bitwarden.sh start"

Your server should be up and using the Let's Encrypt certificate. If there are issues, see the Troubleshooting section at the end of this post.

Set up the renewal hook

Certbot runs scripts in /etc/letsencrypt/renewal-hooks/deploy/ when a certificate is successfully renewed. We will use one to update the Bitwarden certificate and restart the service.

Add a file named bitwarden.sh to that directory. Replace bitwarden.example.com on line 2 with your server's FQDN:

#!/bin/bash -eu
FQDN="bitwarden.example.com"
CERT_DIR="/opt/bitwarden/bwdata/ssl/$FQDN"

if [ "$(basename "$RENEWED_LINEAGE")" == "$FQDN" ]; then
cp "$RENEWED_LINEAGE/privkey.pem" "$CERT_DIR/private.key"
cp "$RENEWED_LINEAGE/fullchain.pem" "$CERT_DIR/certificate.crt"
cp "$RENEWED_LINEAGE/chain.pem" "$CERT_DIR/ca.crt"
chown -R bitwarden:bitwarden "$CERT_DIR"
docker compose -f /opt/bitwarden/bwdata/docker/docker-compose.yml restart nginx
fi

Deploy hooks run when any certificate is renewed, including those for other services running on your system. The check for RENEWED_LINEAGE limits this hook to your Bitwarden certificate renewal.

Set permissions on the file:

sudo chown root:root /etc/letsencrypt/renewal-hooks/deploy/bitwarden.sh
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/bitwarden.sh

Renew your certificate to test the renewal hook:

sudo certbot renew --force-renewal --cert-name $FQDN

If it worked, you will see output similar to this:

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/bitwarden.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for bitwarden.example.com
Hook 'deploy-hook' ran with error output:
Container bitwarden-nginx Restarting
Container bitwarden-nginx Started

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all renewals succeeded:
/etc/letsencrypt/live/bitwarden.example.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Troubleshooting

If you are following these instructions with an existing Bitwarden instance, your configuration may be different and the steps above will require some modifications. Here is a overview of how the installer sets up the /opt/bitwarden/bwdata directory. Some items are omitted for brevity.

/opt/bitwarden/bwdata/
├── docker/
│   ├── docker-compose.yml # mounts cert dirs into the nginx container
├── letsencrypt/ # contains certs from the built-in certbot container
├── nginx/
│   └── default.conf # references the certs that are mounted
└── ssl/
   ├── bitwarden.example.com/ # exists if you chose to bring your own cert
   │ ├── ca.crt
   │ ├── certificate.crt
   │ └── private.key
   └── self/ # exists if you chose self-signed cert
   └── bitwarden.example.com/
   ├── certificate.crt
   └── private.key

If your server does not start after replacing the certificates, switch to the bitwarden user and run this command to check the nginx logs for errors about loading a file:

docker compose -f /opt/bitwarden/bwdata/docker/docker-compose.yml logs nginx

Modify your /opt/bitwarden/bwdata/nginx/default.conf or move your certificate files around so that the nginx directives point to the correct location.

Filesystem PathNginx Config Equivalent
/opt/bitwarden/bwdata/ssl/bitwarden.example.com/.../etc/ssl/bitwarden.example.com/...

If you used the instructions in the Install Bitwarden section, it should look like this:

ssl_certificate         /etc/ssl/bitwarden.example.com/certificate.crt;
ssl_certificate_key /etc/ssl/bitwarden.example.com/private.key;
ssl_trusted_certificate /etc/ssl/bitwarden.example.com/ca.crt; # missing if self-signed