TOC
- Introduction
- Why Use Password Manager
- Why Dockerized Setup
- Prerequisites
- Initial Setup
- Install Docker
- Docker Compose File
- Nginx Configuration
- Get SSL Certificates
- Let's Go
- Access Vaultwarden
- Download Bitwarden Clients
- Access Portainer
- Take Backup
- Update
- References
Introduction
Bitwarden is a very popular open source password manager. Although, official Bitwarden server (written in C#) can be self-hosted, it requires a powerful server to do so. Vaultwarden server is an alternate implementation of Bitwarden server. Vaultwarden, on the other hand, is written in rust and requires less than 100 MB of memory to run.
Portainer will be used to manage Vaultwarden. Nginx will act as a reverse proxy for Vaultwarden and Portainer. Certbot will be used to get SSL certificates. By the way, Portainer is totally optional and you can skip it if you don't want it.
Bitwarden hides some important features such as the ability to add TOTP and file attachments behind paid subscription. A lot of these paid-only Bitwarden features are available for free in Vaultwarden. While not every feature of Bitwarden is ported to Vaultwarden yet, I feel like all the important ones are.
Also, all the official Bitwarden client apps and browser extensions work seamlessly with Vaultwarden.
Why Use Password Manager
Say, you use the same password on multiple websites and one of those websites follows bad security practices and stores your password in plain text or as md5 hash (yes, this still happens!) or in some other easily crackable form. If that website gets hacked, hackers will use your username-password combination or email-password combination on other websites and try to access your other accounts. This is called "credential stuffing attack" and it happens all the time.
There are two things that can save you from this attack:
- Using unique, long and random passwords everywhere
- Using 2 factor authentication (preferably TOTP codes)
A password manager can help you here big time. Almost all password managers have a password generator feature which can generate random passwords and you can even customize it according to the account's requirements (for example, some websites demand password to be at least 12 characters long with at least 1 number and 1 special character and so on). And almost all password managers allow you to store TOTP seeds in them to get 2FA codes.
All reputed password managers store your sensitive data in an encrypted database, aka vault. To encrypt this vault, you need to provide a master password (more on that below). An encryption key is derived from this master password and that key is then used to encrypt the vault. This encrypted vault is decrypted only on client devices and only when correct master password is provided. The password manager company itself can not decrypt it and only the encrypted copy of the vault exists on their servers. All reputed password managers are also routinely audited by third-party companies to make sure everything is on the up and up.
I will go so far as to say that if the password manager server and application are implemented correctly and you're using a unique, long and random master password for your vault, even if the password manager company gets hacked and the vaults are leaked, they will be of no use to the hackers. Even if all the supercomputers in the world started working together to break the encryption of your vault (by either brute-forcing the master password or the encryption key), it will still take them millions of years!
A little about your master password: Since the security of all of your sensitive data depends on your master password, it should be unique and long and random. It should not be guessable by other people. That means it should not be: a song lyric, a dialog from a movie/series/play, a quote by somebody, a line from a book, etc. Come up with a long sentence (of at least 7 words) by yourself and use it as your master passord or use something like diceware (use at least 7 words).
Password managers are also convenient and easy to use. They are available for almost all platforms like Android, iOS, Linux, Windows and MacOS and their browser extensions are available for all major browsers. Many password managers also have autofill feature so you don't even need to type the password. You can also just copy the password from the password manager and then paste it yourself if you want to though.
There are only 2 password managers I recommend:
- Bitwarden or Vaultwarden (explained in this blog post)
- KeepassXC
Why Dockerized Setup
Well, Vaultwarden recommends a dockerized setup. It also require a reverse proxy. I'm familiar with Nginx so I chose that. While I was already familiar with installing Nginx and Certbot on the host machine and letting Nginx communicate with docker containers that way, I thought I will go all the way and dockerize Nginx and Certbot as well.
As the cherry on top, I also included Portainer which is used to manage other docker containers. We will use Portainer to stop Vaultwarden when we're not using it to reduce our attack surface. I'm just being paranoid here that somebody will find some 0-day vulnerability and attack my Vaultwarden instance. But chances of someone finding a 0-day vulnerability in Vaultwarden plus chances of them attacking my private instance whose address I'm not advertising are minuscule. As long as your Vaultwarden vault is secured with unique, long and random master password and you have enabled 2FA on your Vaultwarden account, there is no need to worry.
Prerequisites
I'm assuming you have a new VPS running Ubuntu Server 24.04 LTS. I'm also assuming you have a domain name in your posession, something like example.com. You'll need to create 2 subdomains: 1 for Portainer (p.example.com) and 1 for Vaultwarden (v.example.com). Point A records of the subdomains to your server's IPv4 address and AAAA records to your server's IPv6 address. In the commands, code and configuration files given below, replace example.com with your own domain name.
Initial Setup
You can follow my Initial Server Setup blog post if you need to and then come back here.
Allow ssh, http and https traffic to come in and enable UFW firewall.
sudo ufw allow in ssh
sudo ufw allow in http
sudo ufw allow in https
sudo ufw enable
Install Docker
sudo apt install docker.io docker-compose-v2
sudo systemctl status docker
sudo usermod -aG docker ubuntu
sudo reboot
Docker Compose File
First create all the directories that will be needed right in your home directory.
cd ~
mkdir -p ~/mydocker/nginx/conf
mkdir -p ~/mydocker/certbot/www
mkdir -p ~/mydocker/certbot/conf
mkdir -p ~/mydocker/portainer
mkdir -p ~/mydocker/vaultwarden/vw-data
Create a docker volume for portainer:
docker volume create portainer_data
Here's the full docker compose file. Save it as ~/mydocker/compose.yaml :
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
environment:
TZ: "Asia/Kolkata" # Replace this value with your timezone.
DOMAIN: "https://v.example.com/"
SIGNUPS_ALLOWED: "true" # Set to true only when registering. Then set to false.
volumes:
- ./vaultwarden/vw-data:/data
depends_on:
webserver:
condition: service_started
networks:
- internal
portainer:
image: portainer/portainer-ce:lts
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
depends_on:
webserver:
condition: service_started
networks:
- internal
webserver:
image: nginx:latest
ports:
- 80:80
- 443:443
restart: unless-stopped
environment:
TZ: "Asia/Kolkata" # Replace this value with your timezone.
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf/:/etc/nginx/conf.d/:ro
- ./certbot/www/:/var/www/certbot/:ro
- ./certbot/conf/:/etc/nginx/ssl/:ro
networks:
- internal
- external
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" # Renew certificates every 12 hours
depends_on:
webserver:
condition: service_started
networks:
- internal
- external
volumes:
portainer_data:
external: true
networks:
internal:
driver: bridge
internal: true
external:
driver: bridge
As you can see, Vaultwarden will not have internet access to reduce the attack surface. One downside of this will be that favicons won't show up alongside your vaultwarden entries. But I can live with that.
Nginx Configuration
Here's the Nginx configuration file. Save it as ~/mydocker/nginx/nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
server_tokens off;
# Add Modern SSL config using Mozilla SSL config generator.
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_prefer_server_ciphers off;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# misc
#resolver 127.0.0.1;
resolver 1.1.1.1 9.9.9.9 1.0.0.1 valid=300s;
resolver_timeout 60s;
keepalive_timeout 65;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
Here's Nginx configuration for Vaultwarden. Save it as ~/mydocker/nginx/conf/vaultwarden.conf.
upstream vaultwarden-default {
zone vaultwarden-default 64k;
server mydocker-vaultwarden-1;
keepalive 2;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
server {
listen 80;
listen [::]:80;
server_name v.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
# Temporarily comment out following 2 lines before getting SSL certificate for the first time. After getting it, uncomment following 2 lines.
#listen 443 ssl;
#listen [::]:443 ssl;
http2 on;
server_name v.example.com;
# Temporarily comment out following 3 lines before getting SSL certificate for the first time. After getting it, uncomment following 3 lines.
#ssl_certificate /etc/nginx/ssl/live/v.example.com/fullchain.pem;
#ssl_certificate_key /etc/nginx/ssl/live/v.example.com/privkey.pem;
#ssl_trusted_certificate /etc/nginx/ssl/live/v.example.com/chain.pem;
client_max_body_size 525M;
# Proxy settings that forward requests to Vaultwarden.
location / {
proxy_http_version 1.1; # Set
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Forward requests to Vaultwarden docker container
proxy_pass http://vaultwarden-default;
}
# Optionally add extra authentication besides the ADMIN_TOKEN
# Remove the comments below `#` and create the htpasswd_file to have it active
#
# DO NOT add a trailing /, else you will experience issues
#location /admin {
# # See: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/
# auth_basic "Private";
# auth_basic_user_file /path/to/htpasswd_file;
#
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection $connection_upgrade;
#
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# proxy_pass http://vaultwarden-default;
#}
}
Here's Nginx configuration for Portainer. Save it as ~/mydocker/nginx/conf/portainer.conf.
upstream portainer-default {
zone portainer-default 64k;
server mydocker-portainer-1:9000;
keepalive 2;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
server {
listen 80;
listen [::]:80;
server_name p.example.org;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
# Temporarily comment out following 2 lines before getting SSL certificate for the first time. After getting it, uncomment following 2 lines.
#listen 443 ssl;
#listen [::]:443 ssl;
http2 on;
server_name p.example.org;
# Temporarily comment out following 3 lines before getting SSL certificate for the first time. After getting it, uncomment following 3 lines.
#ssl_certificate /etc/nginx/ssl/live/p.example.org/fullchain.pem;
#ssl_certificate_key /etc/nginx/ssl/live/p.example.org/privkey.pem;
#ssl_trusted_certificate /etc/nginx/ssl/live/p.example.org/chain.pem;
# Proxy settings that forward requests to Portainer
location / {
# Use HTTP version to 1.1
proxy_http_version 1.1; # Set
# Upgrade header to support WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Set Host header as original host
proxy_set_header Host $host;
# Bypass proxy cache
proxy_cache_bypass $http_upgrade;
# Forward requests to Portainer docker container
proxy_pass http://portainer-default;
}
}
Get SSL Certificates
cd ~/mydocker/
docker compose up -d webserver
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d p.example.org
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d v.example.org
docker compose run --rm certbot renew --dry-run
docker compose down
Uncomment those 2 listen lines and 3 SSL lines in both portainer.conf and vaultwarden.conf files above.
Let's Go
Start all the containers:
cd ~/mydocker/
docker compose up -d
Check the logs:
cd ~/mydocker/
docker compose logs -f
If you want to check logs of each individual container separately:
cd ~/mydocker/
docker compose logs webserver -f
docker compose logs certbot -ft
docker compose logs portainer -f
docker compose logs vaultwarden -f
See which containers are running:
cd ~/mydocker/
docker compose ps
Access Vaultwarden
Visit https://v.example.com in your browser. Create an account, log in and then turn on 2FA using authenticator app (TOTP). Make sure you add TOTP of Vaultwarden to some other authenticator app like Aegis Authenticator or Bitwarden Authenticator.
Now that you've made an account in Vaultwarden, open ~/mydocker/compose.yaml file and change SIGNUPS_ALLOWED: "true" to SIGNUPS_ALLOWED: "false" right now so that no one else will be able to create an account on your private Vaultwarden instance.
Write down the email address you used to create the account, master password, TOTP seed and two-step login recovery code on paper and put it in your home locker. Make a copy of it and keep it in your bank locker or in the house of somebody (your best friend or your close relative) who you trust 100%.
Download Bitwarden Clients
Official Bitwarden applications and browser extensions work with Vaultwarden. Make sure you select the "self-hosted" option and enter https://v.example.com as the server URL. Go to Bitwarden download page and install client applications for your devices.
Access Portainer
Open https://p.example.com in your browser. Create an account, log in and you'll be shown details of your local docker environment. Click on "Live Connect" button. Then, click on "Containers" button and you'll be shown a list of all the containers. Keep logged in to your Portainer dashboard in your browser so that you can start the Vaultwarden container right before you want to use it and stop it after you're done using. You can also see logs in real time. And, you can also see how much RAM, CPU, disk I/O and network bandwidth each container is consuming.
As you add more containers in the future, managing them will become easier because of Portainer. Have a look around the Portainer dashboard. There are many more things you can do that I'm not covering here.
Take Backup
cd ~/mydocker/
docker compose down
cd ~
sudo tar -vczf mydocker.tar.gz mydocker/
cd ~/mydocker/
docker compose up
You can also take encrypted json backup of Vaultwarden by going to Vaultwarden settings.
Update
Every week or so, check if they have released new version of Vaultwarden/Portainer/Nginx/Certbot, check their release notes, and if everything's fine, take back up first and then run following commands to update your containers.
cd ~/mydocker/
docker compose down
docker compose pull
docker compose up