Self-Hosted Passwords
How to run Bitwarden at home for more than 3 months.
Like many others, I had difficult decisions to make when LastPass restricted free accounts in February 2021. Password management is essential for my mental stability, and I've always been uncomfortable handing secrets to a third party, so I've migrated to vaultwarden on a (firewalled) raspberry pi. Vaultwarden is an open-source Bitwarden implementation, compatible with all the first-party apps.
Here are a few issues I came across and how I solved them, read on if you're interested. If this post reminds me what I've done in a year's time, it will have been worthwhile regardless.
VPN
The vaultwarden server is at home, somewhere under 10.100.1.1/24
, and I
often need to store/fill passwords while I'm out. Having access all the time isn't
essential, as the mobile app will happily cache passwords until the next time it can
connect, but I thought it best to sync as I go in case of sudden unexpected defenestration.
Wireguard is my over-the-top solution, there's an interface on the pi:
[Interface] Address = 10.100.3.1/24 SaveConfig = true PostUp = <boring> PostDown = <boring> ListenPort = <high port> FwMark = <???> PrivateKey = <super secret> [Peer] PublicKey = <not so secret> AllowedIPs = 10.100.3.2/32 Endpoint = <auto-updated>
The defined peer corresponds to my phone, and we have the following designated ranges at home:
10.100.1.1/24
- Networking hardware and default DHCP pool
10.100.2.1/24
- Devices visible externally to Wireguard peers 1
10.100.3.1/24
- Virtual IPs routed to Wireguard peers
1 Currently this isn't implemented, one thing at a time. Maybe a topic for another post.
Hopefully you can see how this will be scalable as my requirements inevitably grow. Wireguard and vaultwarden are hosted on the same pi, which makes things easy. Remember to configure your firewall, kids.
DNS
The lovely ladies and gents at BT only offer us a dynamic IP from one of their prole pools, so writing wireguard peer config that survives a router reboot will require some DDNS. Fine, I have domains I can use, and Cloudflare have a decent API for it.
I'm surprised there's no off-the-shelf script, but it was simple enough to cobble
together: Gitlab snippet.
The piguard.$domain
hostname should point to my home IP, with the relevant
port forwarded to the raspberry pi. You'll have to mentally replace $domain
with an example of your choice.
The Bitwarden app requires a hostname for the self-hosted backend. That's fine too,
we'll just point vault.$domain
at 10.100.3.1
and Bob's your
uncle. Wireguard auto-connects on my phone when I leave Wi-Fi range, so there should be
no downtime. Neat.
PKI
Bitwarden also requires a trusted certificate for the HTTP API. What's more, Certbot won't issue me a Let's Encrypt certificate via the pi because it isn't internet accessible in a useful way. Nothing's ever easy is it?
Thankfully we can get around this restriction by issuing a wildcard certificate on a different server that does host web applications for that domain, and copy across the certificates after every renewal. Simple in theory, harder if you care about defence in depth.
The certificates generated by Certbot are (typically) stored in /etc/letsencrypt
.
These are, quite rightly, only root-accessible. The best solution I have come up with is
to forward a random high port to piguard:22
at home, set up a Let's Encrypt
renewal hook to tar up and scp
the certificates to an unprivileged account 2
on the pi, and use a cron job to extract the certs and restart nginx.
2Allowing root SSH, even passwordless, gives me the heebie jeebies. We all need boundaries.
- Renewal hook
#!/usr/bin/sh # $domain:/etc/letsencrypt/renewal-hooks/deploy/copy-certs tar -czf ~/le-archive.tar.gz --directory /etc/letsencrypt archive/$domain tar -czf ~/le-live.tar.gz --directory /etc/letsencrypt live/$domain scp ~/le-archive.tar.gz ~/le-live.tar.gz piguard:/home/certbot/ rm ~/le-archive.tar.gz ~/le-live.tar.gz
# $domain:~root/.ssh/config ... Host piguard HostName piguard.$domain User certbot Port <high-port>
#!/usr/bin/sh # piguard:~certbot/bin/extract-certs set -e if [ -f ~/le-archive.tar.gz ]; then tar -xvf ~/le-archive.tar.gz --directory /etc/letsencrypt tar -xvf ~/le-live.tar.gz --directory /etc/letsencrypt systemctl restart nginx rm ~/le-archive.tar.gz ~/le-live.tar.gz fi
I did have a more elegant solution nearly working, which used systemd timers to tar, transfer and extract the cert bundle at renewal time, but it didn't quite work. It would have been beautiful, but I prefer working solutions to pretty things, so I scrapped it.
Huge thanks to Wireguard, Let's Encrypt, Bitwarden (especially for supporting self-hosted installations), and vaultwarden (please don't change your name again) which together make these shenanigans possible.
I do realise that I can use DNS challenges to authenticate with Let's Encrypt from an inaccessible server; in fact this is the method I use for the wildcard cert. If I were to renew the cert on the pi, however, I'd still have to do the transfer, but in reverse. I'd rather have temporary password manager outages when my cron breaks instead of downtime on the sites covered by the certificate, so I'm leaving it how it is.
I hope you enjoyed this ramble, see you next time.
—James
09 May 2021