Disclaimer (Tech)

Danger

Proceed with caution, use at your own risk!

This is merely a documentation of my specific setup, i.e. what I found works for me.

You might have entirely different requirements and expectations of security, etc.

Always use your Brain™

Always read up on up-to-date documentation and current best practices. Inform yourself, research, and treat my documentation as what it truly is: a mere info-dump.

Link to original

Rootless Podman

Prerequisites

Make sure you have podman installed and a frontend Caddy instance set up.

Data directories

First off, create all the necessary directories:

mkdir -p ~/containers/vaultwarden/{data,env}

Frontend Caddyfile

I use a frontend caddy instance for reverse proxying to Vaultwarden. Note that the Vaultwarden container expects incoming traffic on port 8000, as specified in its container config.

Therefore, we simply add a section to the (already present) Caddyfile under ~/containers/caddy/config/Caddyfile

~/containers/caddy/config/Caddyfile
{$VAULTWARDEN_DOMAIN} {
	import subdomain-log {$VAULTWARDEN_DOMAIN}
	
	reverse_proxy http://host.containers.internal:8000 {
		# Send the true remote IP to Rocket, so that Vaultwarden can put this in the
		# log, so that fail2ban can ban the correct IP.
		header_up X-Real-IP {remote_host}
	}
}

VAULTWARDEN_DOMAIN : FQDN of the Vaultwarden instance

Containers

Vaultwarden

We can now simply create a main Vaultwarden container.

Environment File

This specifies environment variables available to the container.

Secret information

This file will contain secret information. If you made sure, to secure your server from outside access, you should be fine. Still, you could consider hardening the access to this file even further. You can’t however simply only give root access to the file, as podman runs unprivileged and won’t be able to access the file. SELinux might help in that regard.

Create and initially populate the vaultwarden.env file under the previously created directory ~/containers/vaultwarden

~/containers/vaultwarden/vaultwarden.env
DOMAIN='https://VAULTWARDEN_DOMAIN'
ROCKET_PORT=8080
LOG_FILE=/var/log/vaultwarden/vaultwarden.log

Replace

  • VAULTWARDEN_DOMAIN : FQDN of this Vaultwarden instance

Vaultwarden will then serve the service over this port within the container. We later redirect an outside port to this in the container config.

Container File

Create the file under ~/.config/containers/systemd/vaultwarden.container

~/.config/containers/systemd/vaultwarden.container
[Unit]
Description=Vaultwarden container
After=network-online.target
 
[Container]
AutoUpdate=registry
Image=ghcr.io/dani-garcia/vaultwarden:latest
Exec=/start.sh
EnvironmentFile=/home/user/containers/vaultwarden/vaultwarden.env
Volume=/home/user/containers/vaultwarden/data:/data:Z
Volume=/home/user/containers/vaultwarden/logs:/var/log/vaultwarden:z
PublishPort=8000:8080
 
[Install]
WantedBy=default.target

Replace

Boot it up

Reload

Reload the daemon

As Quadlet files are systemd service files, you need to reload the daemon.

systemctl --user daemon-reload

This generates appropriate .service files.

Tip

Sometimes, this can fail and not generate a .service file. To debug this, immediately drop into the user journal, to see any error messages

systemctl --user daemon-reload --no-block; journalctl --user -f
Link to original

Auto-Update

Auto-Update

If you enabled the auto-update feature using the AutoUpdate key in the .container file, you still need to enable the auto-update timer

systemctl --user enable --now podman-auto-update.timer
Link to original

Linger

Keep it running

As a rootless setup doesn’t use a system-level service, all services would be stopped upon logout.

To prevent this, we must enable-linger (where user is your username, of course):

loginctl enable-linger user
Link to original

Start

Start the service

systemctl --user start name.service
Link to original

Replace

  • name : vaultwarden

Status

Check the status

You can check the status of Podman using

podman ps

and the status of the service itself using either

systemctl --user status name.service

or

journalctl --user -xeu name.service

Tip

Sometimes, the non-service-specific journal can be helpful in debugging a problem. In that case, simply restart the service and immediately drop into the journal:

systemctl --user restart name.service --no-block; journalctl --user -f
Link to original

Replace

  • name : vaultwarden

Restart

Following that, you probably still need to restart the frontend Caddy, as we modified its Caddyfile previously:

systemctl --user restart caddy.service

Set it up

You should (hopefully) now be able to access your Vaultwarden instance.

Debug

Tip

If something doesn’t work right away, try checking the statuses of the caddy, and the (pod’s) container service(s).

You can also prepend an additional portion in front of all the content to the respective Caddyfiles, enabling more verbose error outputs.

{
	debug
}
Link to original

You can now perform administrative tasks using the admin console, although you’d have to access it from the server directly, as per my advanced Caddyfile.

Hardening

Warning

Always refer to up-to-date information and best practices and also consider reading up on the official upstream Vaultwarden documentation. The Disclaimer applies here, too.

The file, we expand upon, is the Frontend Caddyfile, as the backend is simply the Vaultwarden container itself (served by Rocket internally). The added/modified portions are highlighted, to enable quick expansion of an already existing (and hopefully working) ~/containers/caddy/config/Caddyfile file:

~/containers/caddy/config/Caddyfile
# In combination with the `import admin_redir` statement, this only allows access to the admin interface from local networks
(admin_redir) {
	@admin {
		path /admin*
		not remote_ip private_ranges
	}
	redir @admin /
}
 
{$VAULTWARDEN_DOMAIN} {
	import subdomain-log {$VAULTWARDEN_DOMAIN}
 
	# This setting may have compatibility issues with some browsers
	# (e.g., attachment downloading on Firefox). Try disabling this
	# if you encounter issues.
	encode zstd gzip
	
	# Uncomment to improve security (WARNING: only use if you understand the implications!)
	# If you want to use FIDO2 WebAuthn, set X-Frame-Options to "SAMEORIGIN" or the Browser will block those requests
	header / {
		# Enable HTTP Strict Transport Security (HSTS)
		Strict-Transport-Security "max-age=31536000;"
		# Disable cross-site filter (XSS)
		X-XSS-Protection "0"
		# Disallow the site to be rendered within a frame (clickjacking protection)
		X-Frame-Options "SAMEORIGIN"
		# Prevent search engines from indexing (optional)
		X-Robots-Tag "noindex, nofollow"
		# Disallow sniffing of X-Content-Type-Options
		X-Content-Type-Options "nosniff"
		# Server name removing
		-Server
		# Remove X-Powered-By though this shouldn't be an issue, better opsec to remove
		-X-Powered-By
		# Remove Last-Modified because etag is the same and is as effective
		-Last-Modified
	}
 
	# Uncomment to allow access to the admin interface only from local networks
	import admin_redir
 
	# Proxy everything to Rocket
	reverse_proxy http://host.containers.internal:8000 {
		# Send the true remote IP to Rocket, so that Vaultwarden can put this in the
		# log, so that fail2ban can ban the correct IP.
		header_up X-Real-IP {remote_host}
	}
}

VAULTWARDEN_DOMAIN : FQDN of the Vaultwarden instance

You could in theory also not terminate the TLS chain.

Disable registration

As you probably don’t want anyone to register an account uninvited, you should consider disabling registrations. This preserves the invite functionality.

You can either do that through the admin panel, or by setting SIGNUPS_ALLOWED=false in the Environment variables.

Disable password hints

To disable password hints, which can definitely compromise security, especially with non-random passwords (which you should of course never use), set SHOW_PASSWORD_HINT=false in the Environment variables, or disable it using the admin panel.

Redact token from logs

According to the official hardening guide, the access_token parameter should be redacted from logs.

You can do this within the Caddyfile. Simply replace the import subdomain-logs line with the following snippet:

log {
	hostnames {$VAULTWARDEN_DOMAIN}
	output file /var/log/caddy/{$VAULTWARDEN_DOMAIN}.log
	
	format filter {
		wrap json
		fields {
			request>uri query {
				delete access_token
			}
			
			request>headers>Cookie cookie {
				replace session REDACTED
				delete secret
			}
		}
	}
}

Rate limit login attempts

To prevent brute-force attacks, a rate limit at which login attempts can be made, should be employed. After the limit is hit, you’d need to wait the specified time before being able to try again.

MFA

When using Multi-Factor-Authentication (or colloquially referred to as 2FA / Two-Factor-Authentication), the client uses two requests.

To match a value of 5 requests before the timeout is hit, I increased this value to 10. This should not be a problem, as the number of passwords an attacker would need to try to reliably brute-force my password (with an entropy of over 80 bits), is much, much, much higher.

Add the parameters to the ~/containers/vaultwarden/vaultwarden.env file.

~/containers/vaultwarden/vaultwarden.env
LOGIN_RATELIMIT_MAX_BURST=10
LOGIN_RATELIMIT_SECONDS=60
ADMIN_RATELIMIT_MAX_BURST=10
ADMIN_RATELIMIT_SECONDS=60

Fail2Ban

Install and set up Fail2Ban.

First, try logging in with a random username and password and look for a line regarding the failed attempt within the log file ~/containers/vaultwarden/data/vaultwarden.log ($LOG_FILE, specified in the Environment file), akin to

~/containers/vaultwarden/data/vaultwarden.log
[YYYY-MM-DD hh:mm:ss][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.

Log vs. Systemd

As per the official documentation on Fail2Ban, you could use systemd-journal instead of a log file. However, as Fail2Ban is installed at root-level, Fail2Ban would have a hard time quering the journal using systemctl --user. I found it more convenient to use a log file, as root can definitely read the user-owned file.

Filter

Create a new file vaultwarden.local under Fail2Ban’s filter directory /etc/fail2ban/filter.d

/etc/fail2ban/filter.d/vaultwarden.local
[INCLUDES]
before = common.conf
 
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

Jail

Create a new file vaultwarden.local under Fail2Ban’s jail directory /etc/fail2ban/jail.d

/etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
port = 80,443,1880,1443,8000,8080
filter = vaultwarden
logpath = /home/user/containers/vaultwarden/logs/vaultwarden.log

Replace

SELinux

I ran into some problems with fail2ban.service not being able to read the log file, because of SELinux.

This is a good thing. Normally.

To create policies for that, simply run the Restart command and immediately after check the output of journalctl -xe.

You should see a line containing the keywords avc and denied. Simply copy this line and generate an SELinux policy:

audit2allow -M local << _EOF_
PASTE YOUR LINE HERE
_EOF_

Install it:

sudo semodule -i local.pp

I had to repeat this process once more, as another permission was missing, which means I ended up pasting two audit lines, before typing _EOF_ to signify the end of input.

Hiding under a subdirectory