Self hosted Vaultwarden password manager with Podman
Introduction
In the last blog post I described how to install Bitwarden with Docker. There is a lighter alternative to Bitwarden server called Vaultwarden and it's compatible with official Bitwarden client. Other differences between those two can be easily found online, so I won't go into details here. To make things more interesting, this instruction will show how to install Vaultwarden with Podman.
OS used: Debian 12
Software used: Podman 4.3.1, Vaultwarden 1.32.7
Source
- Official Vaultwarden Wiki page on GitHub
Prerequisites
Create new virtual machine for example using this tutorial - Fast initialization of Debian VM using Ansible.
Create a Traefik container with Podman. The recommended way to enable HTTPS for Vaultwarden container is to run it behind a reverse proxy that handles HTTPS connections. Use this tutorial to setup Traefik - Traefik reverse proxy with Podman.
Following tutorials above, you should have following elements ready to run Vaultwarden:
- VM with Podman
- Traefik container configured with Cloudflare to get Let's Encrypt certificate
- Open port 443 on a firewall to allow connections to Traefik
Vaultwarden hardware requirements
Vaultwarden is lightweight, so it should run nicely on VM with following resources:
- disk size at least 10GB (it uses docker images, so remember to cleanup old images after updates)
- RAM at least 512MB
- CPU at least 1 core
Creating data directory
Create directory for Vaultwarden data:
$ folderpath=/mnt/vaultwarden-data && \
sudo mkdir -m 700 ${folderpath} && \
sudo chown <username>:<username> ${folderpath}
-m - set folder permissions
<username> - enter username of user using Podman
Creating secret for mail notifications
In this example we will use Gmail for mail notifications sent by Vaultwarden. For this, you need to create a Gmail application password. You can generate such password after you have activated 2 Step Authentication in Gmail Account. After you've generated password for your Vaultwarden, store it into Podman secret (use read command to prevent sensitive data from appearing in your Bash history):
$ read -p "Enter password: " password && \
echo -n ${password} | podman secret create vaultwarden-gmail -
echo -n - do not output the trailing newline, otherwise you would have \n at the end of token
Dash - at the end of podman secret create <secret-name> - is important. It means that this command will read data from stdin.
Creating container
Create a script file containing the command to run the Vaultwarden container. Put this script in a folder vaultwarden that you'll create in your home folder. Using a script makes it easier to manage and modify the command.
$ mkdir ~/vaultwarden && \
vim ~/vaultwarden/vaultwarden-container.sh
#!/bin/bash
podman run -d \
--name vaultwarden \
--user "$(id -u)":"$(id -g)" \
--userns=keep-id \
--env ROCKET_PORT=8080 \
--env DOMAIN="https://vaultwarden.exampledomain.com" \
--env SMTP_HOST=smtp.gmail.com \
--env SMTP_PORT=587 \
--env SMTP_SECURITY=starttls \
--env SMTP_FROM=[email protected] \
--env SMTP_USERNAME=[email protected] \
--secret=vaultwarden-gmail,type=env,target=SMTP_PASSWORD \
--env SIGNUPS_VERIFY=true \
--volume /mnt/valutwarden-data:/data \
--net reverse-proxy \
--label traefik.enable=true \
--label traefik.http.routers.vaultwarden-https.tls=true \
--label traefik.http.routers.vaultwarden-https.tls.certresolver=cloudflare \
--label traefik.http.routers.vaultwarden-https.rule="Host(\`vaultwarden.exampledomain.com\`)" \
--label traefik.http.routers.vaultwarden-https.entrypoints=websecure \
--label traefik.http.services.vaultwarden-https.loadbalancer.server.port=8080 \
docker.io/vaultwarden/server:1.32.7
-d- start container in detached mode (detach from it after starting)--user "$(id -u)":"$(id -g)"- start container as a user running podman command, to run the container with the minimum required privileges (good security practice)--userns=keep-id- map host user to user inside container for proper volume permissions. Podman adds an entry for the user into the container's/etc/passwdfile.--env ROCKET_PORT=8080- Rocket is a web server run by Vaultwarden and by default it's run on port 80. Because we used--useroption to run the container, and user can't bind ports lower than 1024 within the container, the port was changed to a value above 1024.--env DOMAIN="https://vaultwarden.exampledomain.com"- set this environment variable to your Vaultwarden host's domain name--env SMTP_HOST=smtp.gmail.com- use Gmail for mail notifications--env SMTP_PORT=587- Gmail SMTP port--env SMTP_SECURITY=starttls- use TLS for SMTP security--env SMTP_FROM- your email addres which to use to send mail notifications--env SMTP_USERNAME- your email addres which to use to send mail notifications--secret=vaultwarden-gmail,type=env,target=SMTP_PASSWORD- secret with password for Gmail access--env SIGNUPS_VERIFY=true- new Vaultwarden users need to verify their email address upon registration--volume /host/path:/container/path- mount folder from host inside container--net <network>- use specific containers network that also includes Traefik container--label traefik.enable=true- enable exposing container to Traefik--label traefik.http.routers.<name>.tls=true- enable tls--label traefik.http.routers.<name>.tls.certresolver- specify provider used for obtaining certificate, in our case it's cloudflare--label traefik.http.routers.<name>.rule=Host- specify which domain name use for the certificate. This label uses backticks`, so use escape symbol to use them\`.--label traefik.http.routers.<name>.entrypoints- define which entry point to use in Traefik for this container. Here we use port defined during creation of Traefik container, which was namedwebsecure.--label traefik.http.services.<name>.loadbalancer.server.port- specify which port to forward to Traefikdocker.io/vaultwarden/server:1.32.7- specify image version from Docker Hub to use for the container
Run your script to start the container:
$ bash ~/vaultwarden/vaultwarden-container.sh
Check if the container was created successfully:
$ podman ps
Starting container with every system boot
First, create a folder for your user to store systemd files (because Podman is run as user):
$ mkdir -p ~/.config/systemd/user
Create a script file containing the command to generate systemd service and write this service to a file:
$ vim ~/vaultwarden/vaultwarden-container-service.sh
#!/bin/bash
# Generate systemd service
# podman generate systemd --new --name CONTAINER_NAME > /path/to/file.service
podman generate systemd --new --name vaultwarden > ~/.config/systemd/user/vaultwarden.service
# Reload user's systemd unit list and enable newly generated service
systemctl --user daemon-reload
systemctl --user enable vaultwarden.service
systemctl --user start vaultwarden.service
Start you script:
$ bash ~/vaultwarden/vaultwarden-container-service.sh
Check if service is running:
$ systemctl --user status vaultwarden.service
Important: Finally, reboot your system and verify that the container is running after reboot.
Creating local DNS entry
Enter domain name and IP address of the machine with Vaultwarden container in your local DNS server (to properly resolve this address by machines in your network), for example:
DNS A record: vaultwarden.exampledomain.com | IPv4 address: 192.168.1.23.
Using Vaultwarden
Log in to address https://vaultwarden.exampledomain.com to use Vaultwarden Web UI. Create new user account. Because email verification is enabled, you'll receive an email with a link that you need to click to activate your new account.
With account created, you can install Official Bitwarden extension in your web browser and there enter address of your Vaultwarden server.
Updating Vaultwarden
To update Vaultwarden you need to perform following steps:
- Stop
vaultwarden.service(this will also stop and remove container):$ systemctl --user stop vaultwarden.service - Change version of Vaultwarden image in
~/vaultwarden/vaultwarden-container.shfile. - Run script
~/vaultwarden/vaultwarden-container.sh. - Run script
~/vaultwarden/vaultwarden-container-service.sh. - If the new Vaultwarden container version is working correctly, you can remove old unused images with this command:
$ podman image prune -a