Introduction
In this tutorial you will learn how to install Vaultwarden via a Docker image.
Vaultwarden is an alternative implementation of the Bitwarden serverside which itself is not completely open-source and hides features behind a pay-wall. The Vaultwarden project, maintained by dani-garcia, mimics the client interfaces to be almost fully compatible with the broad range of Bitwarden clients.
DANGER ZONE - READ CAREFULLY
The vaultwarden serverside does (when properly configured) not store sensitive data in plain text at any give point in time. The decryption of the users data happens only on the endpoint either
in an app, browser extension or inside the web application. Make sure you're comfortable with these security parameters before storing critical data and passwords.
IF YOU LOOSE A USERS MASTER KEY, ALL THEIR DATA IS LOST - FOREVER. Enabling existing work arounds for this issue undermines many, if not all, security benefits of the applications design.
What will be achieved
- Install and set up Docker.
- Configure Traefik, ModSecurity and Vaultwarden
- Profit
Prerequisites
- Hetzner Account
- Control of a publicly resolvable domain
- SMTP Server (can be any free webmail service if you don't have your own server)
Step 1 - Setup new Server
Do not host Vaultwarden and other applications on the same host if not absolutely required. The attack surface should be as small as possible and any added complexity from other applications will introduce unnecessary uncertainty.
- Open Hetzner Cloud Console
- Click "New Project +" and create a new project
- Click "Add Server" and select a datacenter which is closest to your users
- Select Ubuntu 24.04 as OS
- Depending on the amount of users, select at least a CPX21 instance
- Go to "SSH keys" and paste your SSH key. On Windows 10 you can get it via powershell
cat $env:USERPROFILE\.ssh\id_rsa.pub
- Go to "Cloud config" and insert the code below
- Name the server appropriately and click "Create & Buy now"
#cloud-config
package_upgrade: true
apt:
sources:
docker.list:
source: deb [arch=amd64] https://download.docker.com/linux/ubuntu $RELEASE stable
keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg-agent
- software-properties-common
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
- fail2ban
groups:
- docker
system_info:
default_user:
groups: [docker]
This cloud-init
script will take care of the Docker installation, so its a controlled process which is less prone to error.
If you want to install Docker manually, you can refer to the tutorial "Installing Docker on Ubuntu/Debian".
Step 2 - Set DNS Records
Setting the DNS records now gives the DNS system enough time to propagate the changes far enough so that the Let's Encrypt resolvers will resolve the hostname to your IP. This is necessary to obtain a valid SSL certificate to encrypt client connections and proof authenticity of our host system.
Go to the admin panel of the company that hosts your domain name and create an A record with the subdomain of your choosing and the IPv4 address of the newly created server.
Step 3 - Setup the docker-compose.yml file
Opinions on this matter may differ but you could place your docker-compose.yml
file virtually anywhere. We will use the /opt
directory.
-
Create a folder called
vaultwarden
in/opt
mkdir /opt/vaultwarden && cd /opt/vaultwarden
-
Create necessary files
mkdir /opt/docker && mkdir /opt/docker/waf-rules touch /opt/docker/waf-rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf touch /opt/docker/waf-rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf mkdir /opt/docker/le && chmod -R 600 /opt/docker/le
-
Create log directories
mkdir /opt/docker/waf touch /opt/docker/waf/waf.log && chmod o+w /opt/docker/waf/waf.log
-
Add a
docker-compose.yml
filenano docker-compose.yml
Copy & paste the code below to the new
docker-compose.yml
file. You do not need to edit anything as we will add the variables in an additional file in the next step. If you want to receive notifications, edit the SMTP keys as described in the table below the code.The
docker-compose.yml
file will create three containers with the imagestraefik
,owasp/modsecurity-crs
, andvaultwarden/server
.services: traefik: image: traefik:latest container_name: traefik command: - --providers.docker=true - --providers.docker.exposedByDefault=false - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --certificatesresolvers.myresolver.acme.tlschallenge=true - --certificatesresolvers.myresolver.acme.email=${LETS_ENCRYPT_MAIL} - --certificatesresolvers.myresolver.acme.storage=acme.json - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json - --log.level=DEBUG restart: unless-stopped ports: - 80:80 - 443:443 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /opt/docker/le:/letsencrypt waf: image: owasp/modsecurity-crs:apache container_name: waf environment: PARANOIA: 1 ANOMALY_INBOUND: 10 ANOMALY_OUTBOUND: 5 PROXY: 1 REMOTEIP_INT_PROXY: "172.20.0.1/16" BACKEND: "http://vaultwarden:80" BACKEND_WS: "ws://vaultwarden:80/notifications/hub" ERRORLOG: "/var/log/waf/waf.log" volumes: - /opt/docker/waf:/var/log/waf - /opt/docker/waf-rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:/etc/modsecurity.d/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf - /opt/docker/waf-rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf:/etc/modsecurity.d/owasp-crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf labels: - traefik.enable=true - traefik.http.middlewares.redirect-https.redirectScheme.scheme=https - traefik.http.middlewares.redirect-https.redirectScheme.permanent=true - traefik.http.routers.vw-ui-https.rule=Host(`${VAULTWARDEN_DOMAIN}`) - traefik.http.routers.vw-ui-https.entrypoints=websecure - traefik.http.routers.vw-ui-https.tls=true - traefik.http.routers.vw-ui-https.service=vw-ui - traefik.http.routers.vw-ui-http.rule=Host(`${VAULTWARDEN_DOMAIN}`) - traefik.http.routers.vw-ui-http.entrypoints=web - traefik.http.routers.vw-ui-http.middlewares=redirect-https - traefik.http.routers.vw-ui-http.service=vw-ui - traefik.http.routers.vw-ui-https.tls.certresolver=myresolver - traefik.http.services.vw-ui.loadbalancer.server.port=8080 - traefik.http.routers.vw-websocket-https.rule=Host(`${VAULTWARDEN_DOMAIN}`) && Path(`/notifications/hub`) - traefik.http.routers.vw-websocket-https.entrypoints=websecure - traefik.http.routers.vw-websocket-https.tls=true - traefik.http.routers.vw-websocket-https.service=vw-websocket - traefik.http.routers.vw-websocket-http.rule=Host(`${VAULTWARDEN_DOMAIN}`) && Path(`/notifications/hub`) - traefik.http.routers.vw-websocket-http.entrypoints=web - traefik.http.routers.vw-websocket-http.middlewares=redirect-https - traefik.http.routers.vw-websocket-http.service=vw-websocket - traefik.http.routers.vw-websocket-https.tls.certresolver=myresolver - traefik.http.services.vw-websocket.loadbalancer.server.port=3012 vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped environment: WEBSOCKET_ENABLED: "true" SENDS_ALLOWED: "true" PASSWORD_ITERATIONS: 500000 SIGNUPS_ALLOWED: "true" SIGNUPS_VERIFY: "true" ADMIN_TOKEN: "${ADMIN_TOKEN}" # DOMAIN: "SMTP domain host name" # SMTP_HOST: "smtp server" # SMTP_FROM: "sender email e.g: holu@example.com" # SMTP_FROM_NAME: "sender name" # SMTP_SECURITY: "starttls" # SMTP_PORT: 587 # SMTP_USERNAME: "smtp username" # SMTP_PASSWORD: "smtp password" # SMTP_TIMEOUT: 15 LOG_FILE: "/data/vaultwarden.log" LOG_LEVEL: "warn" EXTENDED_LOGGING: "true" TZ: "${TZ}" volumes: - /opt/docker/vaultwarden:/data networks: default: driver: bridge ipam: driver: default config: - subnet: 172.20.0.0/16
If you want to receive notifications, uncomment the SMTP keys and replace the placeholders with your own information:
Placeholder Description SMTP domain host name
Enter the hostname of your SMTP server (Google smtp settings [your mail provider] sender email e.g: holu@example.com
Change to the email you wish to send notifications from sender name
Replace with Vaultwarden or something to instantly tell this e-mail is from the password server smtp username
In most cases your e-mail address; except on AWS SES where you would need to create extra IAM users smtp password
Your smtp password -
Save the variables in
.env
VAULTWARDEN_DOMAIN=vaultwarden.example.com ADMIN_TOKEN=your_admintoken LETS_ENCRYPT_MAIL=holu@example.com TZ=Europe/Berlin
Placeholder Description vaultwarden.example.com
This will take you to the Vaulwarden UI. your_admintoken
We will create the token in the next step. holu@example.com
Put an e-mail you regulary read (SSL notifications will be send there) Europe/Berlin
If you don't live in the GTM+1 Timezone change this -
Put nano editor in background with
CTRL
+T
followed byCTRL
+Z
. -
Generate Admin token and copy output
sudo apt install argon2 echo -n "MySecretPassword" | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
Replace
MySecretPassword
with a password of your choice. -
Copy the output and replace all five occurrences of
$
with$$
to avoid variable interpolation. Example:$$argon2id$$v=19$$m=65540,t=3,p=4$$M29tVXp3ZVl6UC9EZDVRdW1EWE9GQWg2cXVRaXdiMnMrVlIrU2xjbXFyST0$$TZUeinxme5vCB++KdBy5UuBJvcPtqk4xXMfQpeaY6AA
-
Bring nano back to front by typing
fg
and hittingENTER
-
Replace
your_admintoken
with the edited output from above, e.g.:ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$M29tVXp3ZVl6UC9EZDVRdW1EWE9GQWg2cXVRaXdiMnMrVlIrU2xjbXFyST0$$TZUeinxme5vCB++KdBy5UuBJvcPtqk4xXMfQpeaY6AA
-
Save file with
CTRL
+X
and then pressY
andENTER
Step 4 - Configure Fail2Ban
-
Create filter for the vaultwarden application
nano /etc/fail2ban/filter.d/vaultwarden.local
Include the following lines
[INCLUDES] before = common.conf [Definition] failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$ ignoreregex =
-
Create filter for the vaultwarden admin application
nano /etc/fail2ban/filter.d/vaultwarden-admin.local
Include the following lines
[INCLUDES] before = common.conf [Definition] failregex = ^.*Invalid admin token\. IP: <ADDR>.*$ ignoreregex =
-
Create filter for the ModSecurity application
nano /etc/fail2ban/filter.d/waf.local
Include the following lines
[INCLUDES] before = common.conf [Definition] failregex = ^.*\[client <ADDR>\] ModSecurity: Access denied with code 403 .*$ ignoreregex =
-
Create jail for the vaultwarden application
nano /etc/fail2ban/jail.d/vaultwarden.local
Include the following lines
[vaultwarden] enabled = true port = 80,443,8081 filter = vaultwarden banaction = %(banaction_allports)s logpath = /data/vaultwarden.log maxretry = 3 bantime = 14400 findtime = 14400
-
Create jail for the vaultwarden admin application
nano /etc/fail2ban/jail.d/vaultwarden-admin.local
Include the following lines
[vaultwarden-admin] enabled = true port = 80,443 filter = vaultwarden-admin banaction = %(banaction_allports)s logpath = /data/vaultwarden.log maxretry = 3 bantime = 14400 findtime = 14400
-
Create jail for the ModSecurity application
nano /etc/fail2ban/jail.d/waf.local
Include the following lines
[waf] enabled = true port = 80,443 filter = waf action = iptables-allports[name=waf, chain=FORWARD] logpath = /opt/docker/waf/waf.log maxretry = 1 bantime = 14400 findtime = 14400
Reload the ruleset with this command:
sudo systemctl reload fail2ban
Step 5 - Starting Instance
To startup your instance, run:
cd /opt/vaultwarden && docker compose up -d
Traefik needs some time to properly start up, discover all services and issue certificates. This can take up to 5 minutes so please be a little patient. You can run docker ps -a
to check the status and check the logs for errors with docker compose logs
.
Click here for commands to troubleshoot
curl -k https://vaultwarden.example.com:443 curl http://$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vaultwarden):80 docker exec -it traefik wget -qO- http://vaultwarden:80 docker exec -it traefik wget -qO- http://waf:8080 docker exec -it waf curl http://vaultwarden:80 docker inspect vaultwarden_default docker logs traefik
You can now connect to your Vaultwarden instance via any modern web browser by accessing your domain and create an account for yourself:
https://vaultwarden.example.com
To access the admin interface, go here and enter the password from earlier on (in this example "MySecretPassword"):
https://vaultwarden.example.com/admin
There you can manipulate all important settings, however the configuration defined in the docker-compose.yml
is a good starting point. You might want to disable public registration though.
Just go to General Settings
and uncheck Allow new singups
and press save on the bottom of the page.
How to connect through Bitwarden applications
To connect your devices to your new Vaultwarden server, please follow this guide from Bitwarden.
Step 6 - Optional: Tuning ModSecurity
ModSecurity is a web application firewall that can prevent common attacks on web applications such as Vaultwarden. Depending on the protection requirements, it might be necessary to increase
the aggressivity of ModSecurity. In order to do so, increase PARANOIA
or decrease ANOMALY_INBOUND
in the docker-compose.yml
file. Be warned, changes to the default values will require manual tuning of ModSecurity
which will take quite some time to get just right. There is a good guide here.
Step 7 - Enable Hetzner Firewall
-
Open Cloud Console, select your project, go to
Firewall
and click onCreate Firewall
. -
Add three inbound rules to the Firewall:
First rule
Protocol: TCP
Port: 80
IP:Any IPv4
,Any IPv6
Second rule
Protocol: TCP
Port: 443
IP:Any IPv4
,Any IPv6
Third rule
Protocol: TCP
Port: 22
IP:<your-own-IP>
If you don't know your IP address, you can go to https://ip.hetzner.com/ and copy and paste it from there.Note: Residential internet connections will most likely change their public IPv4 address frequently or might even be behind CGNAT. This means you need to change the third firewall rule any time you want to access your server to reflect your current IPv4 address.
-
Apply the Firewall to your server.
- Go to
Apply to
and select+ SELECT RESOURCES
➔Server
. - Select you server and click on
APPLY TO 1 SERVER
.
- Go to
-
Click
Create Firewall
Graphic representation on how this all works
Conclusion
In the previous steps you learned how to create a new server that has Docker installed on it, how to set up Vaultwarden by using a Docker image, and how to set up the Hetzner Firewall.