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.
Make sure you have podman installed and a frontendCaddy 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} }}
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
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:
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.
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} }}
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:
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.
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
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.
Use systemd?
Fail2Ban is also capable of using systemd instead 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.