Building a self-hosted secure password management with Bitwarden and Docker
Over two years ago, I migrated to 1Password to store and manage all my passwords. Since then, I migrated back to KeePass again – as 1Password was quite cumbersome to use for our family’s needs. Too many technical issues, too many weird authentications prompt, and too challenging to rapidly set up for an 8-year-old.
I’ve happily used KeePass since. It’s free, and it’s available on all major platforms. The problem is, it’s very local. Too local for my needs at times. On a mobile device, you need the app (of course), but getting the secured database introduces some additional hurdles. Syncing it manually is tiresome. And with sync, you get out-of-sync issues before you know it.
So, I set out to replace KeePass with something else. And I found Bitwarden.
About Bitwarden
Bitwarden is a service for password management. It’s available for free for teams of up to 2 people, and paid tiers are available all the way to enterprises. It’s very reasonably priced also – starting from $3/month per user.
The Windows app for Bitwarden looks like this:
It has all the usual features, such as browser integration, password generator, and such. It also supports Windows Hello for unlocking the password database, which is a huge plus for me. I use the Logitech Brio webcam for Windows Hello at home.
I was keen to try it out, but then I found out that the service is based on an open-source project. And I’m entirely willing to host stuff for myself if it means I get to save $3 a month! There are numerous forks of this project, and what caught my attention was the Docker-based Bitwarden-rs build. It’s built on Rust, a programming language I know nothing about. I think I’m set for success here.
So, to understand this better myself: You can do any of the following:
- Use Bitwarden as a SaaS-service, by paying the monthly fee
- Self-host Bitwarden with their official image (and pay a monthly fee if you require additional capabilities)
- Self-host Bitwarden with the fork, but not be connected to any of their offerings (and not needing to pay a monthly fee).
The plan
My initial plan was to pull the Docker container, spin it up and call it a day. Perhaps have early lunch. The container is available at Docker Hub as a pre-built image.
I have a few platforms where I can run Docker containers: A couple of Raspberry Pi 4’s, my Synology NAS (based on Linux), a few Windows Servers, and my main Windows 10 desktop. I like the idea of running these critical services on the NAS, as it never gets shut down – and I have a fairly robust backup setup for it.
Building the solution
The Synology NAS essentially gives me a sweet UI for deploying Docker containers. A bit like with Docker Desktop, but in the browser. I use it to host my Unifi Cloud Key in a container, also.
I pulled down the image and spun up a container. I mapped the /data directory to my NAS volume for persistent storage.
All this can be done easily from the command line via the terminal, of course – but the same results are achieved with a few mouse clicks here.
Does it work? Yes, it does. Opening Bitwarden via the browser defaults to an HTTP connection, and it doesn’t work. Which I think is great, as this forces everyone to think about how to enable HTTPS next.
I utilized Let’s Encrypt certificates next to add HTTPS here. There are numerous ways to do this. Usually, I use Certbot from a Linux shell to request and generate the necessary certification files.
As I’m using Synology, it has built-in capability for it. If you’re aiming to build a similar setup without a Synology device, follow the guidance here. Alternatively, you can utilize several reverse proxies listed here. Or perhaps even using Azure AD Application Proxy.
Synology’s Let’s Encrypt wizard is pretty bare bones:
And once done, I’m using Synology’s built-in reverse proxy to map all calls to HTTPS (on my external WAN IP) to HTTP (to the Docker container).
Does it work now? Yes!
But there’s a problem
There’s always a small problem, isn’t there? This time it’s with Android.
Bitwarden works beautifully within the browser (as an extension), in Windows 10, as a website.. but not for Android. It simply refuses to log in. I found this lengthy thread about the issue here. I read all of it, and some people suggested that downgrading the Android app to an older version would work. But why doesn’t it work?
I did suspect it’s something with the Let’s Encrypt certificate. I downloaded the cert files from my Synology (and these are identical to what you would get from Certbot, once you’ve requested a cert):
cert.pem is the public key of the certificate. privkey.pem is the private key of the certificate. And chain.pem is the whole certificate chain.
I tried installing cert.pem to my Android device. Nothing changed – it wouldn’t allow me to log in to my self-hosted Bitwarden instance. I then figured that I’d need to install chain.pem, because it should hold the root certificate, and all intermediate certifies. Perhaps Android, the Bitwarden Android app, or the Bitwarden service is querying through the whole chain and panicking if something is missing?
I downloaded chain.pem on my Android phone, clicked on it, and installed it. And Bitwarden works! Perhaps this is an additional setting that was later removed from the recent builds of the Android-app of Bitwarden, and thus it needs to be manually added by the user now. Obviously I’ll need to do this in the future, if the cert needs to be renewed.
Importing my secrets to Bitwarden
The last step was to import my KeePass secrets. Bitwarden’s web UI supports imports from almost any imaginable platform – including KeePass!
In closing
I’ve had Bitwarden running in my container for several days now. It’s rock stable; but I still need to test the recovery and restore procedures from my backups, should the container fail for some reason.
I use it on everything now: my Android-based tablet, Android phones, Windows 10 devices, and Firefox. It’s very bare bones, but in a good way.
And the container is lightweight – usually consuming just 80-150 MB of RAM. That’s perhaps less than what a single browser consumes.