Complete Guide to Self-Hosting Vaultwarden for Password Management

Complete Guide to Self-Hosting Vaultwarden for Password Management

What is Vaultwarden?

In the realm of digital security, managing passwords securely is paramount. Enter Vaultwarden is a self-hosted password manager designed for individuals and organizations seeking robust encryption and full control over their sensitive data. Unlike cloud-based alternatives, this self-hosted password manager empowers users to safeguard passwords and confidential information on their own servers, ensuring maximum security and privacy.

💡
An automated bash script to setup vaultwarden is created as well

Installing Vaultwarden with Docker

Setup overview

In this article, I will offer a detailed guide, taking you step by step through the process of elevating the configurations to their optimal level. This involves configuring all the recommended security enhancements, such as setting up Vaultwarden self-hosted password manager to operate under a non-root user and following the most robust security practices.

Create a new user

To enhance security, begin by creating a new, unprivileged Linux user specifically for Vaultwarden. This mitigates risks associated with running applications as root.

sudo adduser vaultwarden

Install docker and docker compose

For docker rootless installation script to work correctly, we have to ssh to the user.

💡
Switching to the user without ssh is not enough.

Now, we can execute the automated docker rootless script setup as the newel y created vaultwarden user.

You can get the automated script from my previous article:

Automated docker rootless setup
Docker rootless provides a safe and accessible environment to harness the full potential of Docker.

As we will be using docker-compose to deploy the vaultwarden instance, we have to install docker-compose package as well using:

pip3 install docker-compose
💡
Note: python3-pip package is required for this command to work

docker-compose Vaultwarden setup

For better management, we have to create a new docker-compose.yml file instead of starting Vaultwarden from the cli.

Fix the data dir permissions

Executing Vaultwarden as root within the container is straightforward and doesn't demand any special configurations, given that root possesses the authority to write to any directory.

Running as non-root user is another story. The data directory where all the data will be written should be mounted to the host system to be able to backup our data. This can be done using:

    volumes:
      - /home/vaultwarden/data:/data

The directory /home/vaultwarden/data on our host system will be linked to the /data directory within the container through mounting. However, a concern arises: Docker mounts this as root, which results in Vaultwarden encountering failure. This failure occurs because Vaultwarden doesn't operate as root, leading to a "permission denied" error when attempting to write to the /data directory within the container.
To address this problem, we can use another lightweight Linux image. This service will operate with sudo privileges exclusively during system startup, enabling the assignment of appropriate permissions to the /data directory.

💡
The data dir within the container should be assigned to the group ID of the user under which Vaultwarden operates on the host system. You can get your specific group ID using the id command.
---
version: "3.7"
services:
  change-data-vol-owner:
    image: alpine
    user: root
    group_add:
      - 1007
    volumes:
      - /home/vaultwarden/data:/data
    command: ["sh", "-c", "mkdir -p /data/logs && chown -R 1007:1007 /data"]

This action will assign the ownership of /data to the user and group with the IDs 1007:1007.

And finally we can let Vaultwarden depend on the change-data-vol-owner service using:

    depends_on:
      change-data-vol-owner:
        condition: service_completed_successfully

Create the user inside the container

As we will be running vaultwarden process as 1007:1007, we will not have a user assigned to this PUID/PGID by default inside the container. To do so , we have to build vaultwarden based on the official image and create this user.

This can be done by uing a Dockerfile

FROM vaultwarden/server:1.31.0

RUN echo "vaultwarden:x:1007:1007::/home:/bin/sh" >> /etc/passwd \
    && echo "vaultwarden:x:1007:" >> /etc/group

Deploy Vaultwarden

A new service for Vaultwarden can be created as follows:

---
version: "3.7"
services:
  change-data-vol-owner:
    image: alpine
    user: root
    group_add:
      - 1007
    volumes:
      - /home/vaultwarden/data:/data
    command: ["sh", "-c", "mkdir -p /data/logs && chown -R 1007:1007 /data"]
  vaultwarden:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: vaultwarden
    user: "1007:1007"
    environment:
      - TZ=Europe/Brussels
      - ROCKET_ENV=prod
      - ROCKET_PORT=8080
      - DATA_FOLDER=/data
    volumes:
      - /home/vaultwarden/data:/data
    ports:
      - "127.0.0.1:8081:8080"
    depends_on:
      change-data-vol-owner:
        condition: service_completed_successfully

volumes:
  data: 

The environment variables:

  • TZ is the Time Zone. You can get it by running cat /etc/timezone.
  • DATA_FOLDER define the data directory inside the container.
  • ROCKET_PORT is the port used by Vaultwarden within the container. It's essential to choose a port number higher than 1024 for this purpose. This ensures that Vaultwarden, operating as a non-root user, can successfully bind to the chosen port.

As for the ports section:

    ports:
      - "127.0.0.1:8081:8080"

This will make sure that Vaultwarden will be available at 127.0.0.1:8081 in the host system.

💡
This port should be used when configuring Caddy reverse proxy later.


Furthermore, it's essential to consistently obtain the latest tag from the official Docker website. In my situation, I'm utilizing the 1.29.1 tag.

Vaultwarden hardening and optimization

Read only file system

Implement a read-only file system to prevent unauthorized modifications.

    read_only: true

Limiting the memory

Set memory limits to optimize resource management and prevent over-allocation. .I've set it to 500 Mb by default using:

    deploy:
      resources:
        limits:
          memory: 500M

Reduce the write operations

Minimize write operations to prolong hardware lifespan, especially on devices like Raspberry Pi. This strategy aims to prolong the SD card's lifespan. To achieve this, I've taken the following steps:

  • I've mounted several temporary disks as tmpfs file systems.
  • I've disabled Docker logging using the following method:
    tmpfs:
      - /var/run
      - /var/log:mode=1775
      - /var/tmp:mode=1777
      - /tmp
    logging:
      driver: none

Enable admin page

To enable admin page https://yourdomain.com/admin , we have to generate an admin token first using:

echo -n "MySecretPassword" | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1 | sed 's#\$#\$\$#g'

With MySecretPassword as password (make sure to use a very strong password). This will generate a token similar to:

$$argon2id$$v=19$$m=19456,t=2,p=1$$Y1NtOFduRmE3UEhDY3UwTUR4QjBZSURoMlZRZFBSSHpjWTVrRVZ3Slpqdz0$$xLFtcvI6z1hnd7i3DlmEaP8l83BYBeEfm9RqdGHk0aA

We can generate this using docker as well but you need the Vaultwarden container to be running first:

docker exec -it vaultwarden /vaultwarden hash

Then finally we can use this value in the docker compose file:

      - ADMIN_TOKEN=$$argon2id$$v=19$$m=19456,t=2,p=1$$Y1NtOFduRmE3UEhDY3UwTUR4QjBZSURoMlZRZFBSSHpjWTVrRVZ3Slpqdz0$$xLFtcvI6z1hnd7i3DlmEaP8l83BYBeEfm9RqdGHk0aA
💡
It's highly recommended to keep this value commented and enable it only when needed.

Disable signups

After creating the initial account, we can disabled both signups and invitations. Given that I am the only user of this instance, this action effectively prevents unauthorized individuals from creating accounts on my server.

This can be done with the below:

      - SIGNUPS_ALLOWED=false
      - SIGNUPS_VERIFY=true
      - INVITATIONS_ALLOWED=false
      - SHOW_PASSWORD_HINT=false

Setup 2fa

Upon successfully logging in, it's strongly advised to promptly activate two-factor authentication (2FA) to safeguard your account comprehensively. This can be configured at https://yourdomain.com/#/settings/security/two-factor

Set login rate limit

By default, Vaultwarden has brute-force attacks prevention's but we can modify the default values as well if needed:

      - LOGIN_RATELIMIT_MAX_BURST=10
      - LOGIN_RATELIMIT_SECONDS=60      
      - ADMIN_RATELIMIT_MAX_BURST=10
      - ADMIN_RATELIMIT_SECONDS=60
  • LOGIN_RATELIMIT_MAX_BURST denotes the upper limit of requests permissible in a burst during login or two-factor authentication attempts, while still adhering to the average set in LOGIN_RATELIMIT_SECONDS.
  • LOGIN_RATELIMIT_SECONDS indicates the mean duration, in seconds, between consecutive login requests originating from the same IP address. Once this threshold is reached, Vaultwarden initiates rate limiting for login attempts.
  • ADMIN_RATELIMIT_MAX_BURST same as LOGIN_RATELIMIT_MAX_BURST but for the admin page only.
  • ADMIN_RATELIMIT_SECONDS same as LOGIN_RATELIMIT_SECONDS but for admin page only.

Configure logs

Maintaining logs is a recommended practice, as they prove invaluable for troubleshooting, monitoring server health, and responding to critical failures and security threats.

We can add the following:

      - EXTENDED_LOGGING=true
      - LOG_LEVEL=info
      - LOG_FILE=/data/logs/access.log

Putting everything together

A final docker-compose.yml should look like:

---
version: "3.7"
services:
  change-data-vol-owner:
    image: alpine
    user: root
    group_add:
      - 1007
    volumes:
      - /home/vaultwarden/data:/data
    command: ["sh", "-c", "mkdir -p /data/logs && chown -R 1007:1007 /data"]

  vaultwarden:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: vaultwarden
    user: "1007:1007"
    read_only: true
    environment:
      - ADMIN_TOKEN=$$argon2id$$v=19$$m=19456,t=2,p=1$$Y1NtOFduRmE3UEhDY3UwTUR4QjBZSURoMlZRZFBSSHpjWTVrRVZ3Slpqdz0$$xLFtcvI6z1hnd7i3DlmEaP8l83BYBeEfm9RqdGHk0aA
#      - SIGNUPS_ALLOWED=false
#      - SIGNUPS_VERIFY=true
#      - INVITATIONS_ALLOWED=false
      - SHOW_PASSWORD_HINT=false
      - LOG_LEVEL=info
      - EXTENDED_LOGGING=true
      - LOG_FILE=/data/logs/access.log
      - TZ=Europe/Brussels
      - ROCKET_ENV=prod
      - ROCKET_PORT=8080
      - DATA_FOLDER=/data
      - LOGIN_RATELIMIT_MAX_BURST=10
      - LOGIN_RATELIMIT_SECONDS=60
      - ADMIN_RATELIMIT_MAX_BURST=10
      - ADMIN_RATELIMIT_SECONDS=60
    volumes:
      - /home/vaultwarden/data:/data
    ports:
      - "127.0.0.1:8081:8080"
    deploy:
      resources:
        limits:
          memory: 500M
    tmpfs:
      - /var/run
      - /var/log:mode=1775
      - /var/tmp:mode=1777
      - /tmp
    logging:
      driver: none
    depends_on:
      change-data-vol-owner:
        condition: service_completed_successfully

volumes:
  data: 

Finally, to deploy Vaultwarden you can execute:

docker-compose up -d

The container can be stopped with this command:

docker-compose down

Configure reverse proxy

Vaultwarden cannot function over HTTP; HTTPS is an absolute requirement. To fulfill this need, we can configure Caddy as a reverse proxy that serves an automatically generated Let's Encrypt certificate.

I've already created a full article about this subject:

Automatic HTTPS with caddy as reverse proxy - byteninja
Caddy is a web server and reverse proxy known for its simplicity and powerful features. With automatic HTTPS, easy configuration, and extensive capabilities, Caddy makes it easy to secure and route traffic to the back-end servers.

Automated script

All above steps have been automated for you. The script will do the following:

  • Create a new Linux user where Vaultwarden will be deployed.
  • Automatically install docker in rootless mode.
  • Automatically install docker compose.
  • Configure Vaultwarden docker file.
  • Add cronjobs to start Vaultwarden automatically after reboot.

You can find the script in my GitHub repository.

The only two variables that you may change ( if needed ) are:

#MODIFY IF NEEDED
VAULTWARDEN_USER_ACCOUNT="vaultwarden"
#ONLY USE PORT HIGHER THAN 1024 OR FEW MODIFICATIONS WILL BE REQUIRED
VAULTWARDEN_PORT=8081

With:

  • VAULTWARDEN_USER_ACCOUNT represents the user account freshly created by the script, serving as the host for both the Vaultwarden container and its associated data.
  • VAULTWARDEN_PORT designates the host system's port through which Vaultwarden will be accessible.

Self-hosting Vaultwarden offers compelling benefits, including heightened security through data control, enhanced privacy, and customization options. By managing your own server, you gain offline access to passwords, control over updates, and resource efficiency. While self-hosting demands technical proficiency and ongoing maintenance, it presents an opportunity to learn, integrate with other tools, and tailor the service to your preferences.

Feel free to ask any questions you may have in the comments section, and I'll be happy to assist you.

buy me a coffe