Disclaimer (Tech)
Link to originalDanger
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.
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) Caddy under ~/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
rootaccess 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
DOMAIN='https://VAULTWARDEN_DOMAIN'
ROCKET_PORT=8080
LOG_FILE=/var/log/vaultwarden/vaultwarden.logReplace
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
[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.targetReplace
user: username used for running the rootless podman instance.
Boot it up
Reload
Transclude of Podman#Reload the daemon
Auto-Update
Circular transclusion detected: Podman
Linger
Circular transclusion detected: Podman
Start
Circular transclusion detected: Podman
Replace
name:vaultwarden
Status
Circular transclusion detected: Podman
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.serviceSet it up
You should (hopefully) now be able to access your Vaultwarden instance.
Transclude of Caddy#Debug
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 System Administration applies here, too.
The file, we expand upon, is the Vaultwarden, 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:
# 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 Caddy.
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 Caddy, 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.
LOGIN_RATELIMIT_MAX_BURST=10
LOGIN_RATELIMIT_SECONDS=60
ADMIN_RATELIMIT_MAX_BURST=10
ADMIN_RATELIMIT_SECONDS=60Fail2Ban
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 Vaultwarden), akin to
[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-journalinstead of a log file. However, as Fail2Ban is installed atroot-level, Fail2Ban would have a hard time quering the journal usingsystemctl --user. I found it more convenient to use a log file, asrootcan definitely read the user-owned file.
Filter
Create a new file vaultwarden.local under Fail2Ban’s filter directory /etc/fail2ban/filter.d
[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
[vaultwarden]
enabled = true
port = 80,443,1880,1443,8000,8080
filter = vaultwarden
logpath = /home/user/containers/vaultwarden/logs/vaultwarden.logReplace
user: username used for running the rootless podman instance.
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 Fail2Ban 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.ppI 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.
Use
systemd?Fail2Ban is also capable of using
systemdinstead of log files. The only difficulty is that all my Systemd processes run in user mode.I haven’t yet found a solution of hooking into the user-specific Systemd journals.
It would probably be a cleaner way to do the tracking, though.
Hiding under a subdirectory
I decided against it
Although the official hardening guide argues that this is not security through obscurity, but rather defense in depth, I would disagree. I don’t find these two terms to be mutually exclusive. Security through obscurity doesn’t necessarily require it to be the sole security measure, but rather describes a category of security measures. I, the same as many others, thoroughly disagree that security through obscurity should be considered an effective security measure in our day and age. And although it might not hurt, I categorically try to avoid fooling myself on the effectiveness of employed measures, giving me a false sense of security. I’d rather focus on real and effective measures instead.
The way I see it, defense in depth is already provided with my setup and adding another layer by using subdirectories. Using subdirectories, in my opinion, definitely qualifies as security through obscurity, adds no real security, is redundant, and confuses me more than a potential attacker. As it solely and specifically counts on an attacker not finding a specific subdirectory the instance is hosted under, I could effectively use a randomly generated subdomain for nearly the same effect. I find the reliance on a non-diligent attacker in the day and age of completely automated attacks, port scans, botnets, etc., to be laughable, naive, and possibly ignorant.