Vaultwarden Secure? How to Harden Your Setup
Vaultwarden encrypts vault items on your device, but your admin token and backup file remain the real risks. See the hardening checks that matter.
Vaultwarden secure be? The short answer
Vaultwarden dey secure for the place wey matter pass, because your device dey encrypt every vault item before e reach the server. The server dey store blobs wey e no fit read. Even if person copy the whole database, dem still need master password before dem fit get anything useful from am.
That answer dey cover plenty things, and na the settings you configure be the parts wey fit fail. Admin panel wey dey behind token wey person fit guess. Container port wey you publish to the whole internet. Plaintext config.json. Backup tarball wey dey sit for home directory for the same box. None of these na cryptography problems. All of dem na why self-hosted vaults dey get emptied.
Everything below assume say your install dey work. If you never get one, set am up first with Vaultwarden install guide for VPS, then come back and follow this list from top to bottom.
Server actually wetin e dey store
Vaultwarden dey use Bitwarden data model. E encrypt vault item name, username, password, notes, and URIs with key wey e derive from your master password for client side, before e send any request. E encrypt attachment file contents the same way. Server receive opaque data wey UUID (universally unique identifier) attach to am.
Some things no be ciphertext, and you suppose know exactly which ones:
- Your account email address, for plaintext.
- Your KDF (key derivation function) settings and salt, because client need dem to rebuild the key for next login.
- Server-side hash of the master password hash wey client send, wey e use to authenticate the login itself.
- Metadata: organisation membership, device names, last login times.
- Secret for the two-factor method wey protect Vaultwarden login. E dey inside
twofactortable unencrypted, because server need calculate the expected code and compare am with your own. This one no be the same as TOTP (time-based one-time password) secret wey you store inside a vault item. E dey encrypted like every other field.
The data folder small. For Docker install, na the folder wey you mount for /data.
sudo ls -l /vw-data/db.sqlite3 hold almost all state. attachments/ hold uploaded files, one file for each UUID, and na the only important data class wey no dey database tables. sends/ hold Send attachments and dem suppose temporary. icon_cache/ na throwaway. rsa_key.pem and the other related files sign JWTs (JSON web tokens) of users wey don log in. So anybody wey get copy of that private key fit forge vault login session. config.json only dey exist after you enable the admin page. The project talk am plainly: e hold admin token and your SMTP credentials for plaintext.
So the practical threat model na filesystem access, no be network cryptography. Read access to that one directory go hand over every user email address, their login 2FA secrets, key wey fit forge sessions, and offline copy of every vault wey attacker fit attack at their own time. Every step below dey protect that directory from unauthorized access.
Fix admin token first
/admin na full control panel: user list, invitations, deletion, and every runtime setting. One shared secret dey protect am, and nothing else. No username. No per-user two-factor.
Older guides tell you to generate ADMIN_TOKEN with openssl rand -base64 48. E dey work, and e write the secret as plaintext inside config.json and your compose file. Vaultwarden also accept Argon2 PHC (password hashing competition) string, so the stored value go be hash instead. Generate one against a running container:
docker exec -it vaultwarden /vaultwarden hashOr without touching the running container at all:
docker run --rm -it vaultwarden/server /vaultwarden hashE go ask for password two times, then print one line wey start with $argon2id$. For bare-metal install, run ./vaultwarden hash. If you prefer to use argon2 CLI directly, the project document the OWASP minimum parameters:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1Now na the trap wey fit waste one hour. PHC string full of $ characters, and Docker Compose dey treat $ as variable interpolation. If you paste am without escaping inside environment: block, the value wey reach the container go spoil, so /admin reject token wey you know say correct. Two safe forms dey. Inside docker-compose.yml, double every $:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UIInside .env file, you no need escaping, but use single quotes:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Then put rate limit for the panel and make its session shorter:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20Three bad attempts within five minutes and the panel stop answering that client. The admin session expire after 20 minutes of inactivity.
Better pass all these: turn the page off. Most instances need am once, to configure SMTP and invite the first users, and dem no need am again. To disable am, set neither ADMIN_TOKEN nor DISABLE_ADMIN_TOKEN, remove any "admin_token" key from config.json, then recreate the container. E matter to delete the key from the file because the admin page dey write settings there, and wetin dey inside config.json go override the environment. If you only remove the variable, the page go remain open.
Close registration before anybody discover the domain
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED default na true. Leave am like that and anybody wey reach your domain go fit create account, and their data go dey inside the same db.sqlite3 as your own. Set am to false and add people through invitation from the admin page. This one need working SMTP. INVITATIONS_ALLOWED too na true by default and e dey allow organisation owners invite other people. That one dey okay when you trust your users, and e suppose dey false for single-user instance. If na only specific domains dem suppose ever register, SIGNUPS_DOMAINS_WHITELIST=example.com dey more restricted than open signup and e much weaker than invitation.
SHOW_PASSWORD_HINT na false by default and e suppose remain like that. When e dey enabled, if person enter valid email address for login form, the form go return that account master password hint. This one expose the hint and confirm say the address dey exist.
If your instance run with signup open for any period, open the admin page and check the user list before you assume say na only your account dey there.
Port wey you no mean publish
Docker image dey listen for port 80 inside the container. Bare-metal install dey use ROCKET_PORT=8000 by default. The documented run command dey publish am like this:
--publish 127.0.0.1:8000:80The 127.0.0.1: prefix na the whole point. If you write -p 8000:80 instead, Docker go bind 0.0.0.0, and e go do this by writing DNAT (destination network address translation) rules inside the nat table. Dem rules dey evaluate before the filter chains wey ufw dey manage. So ufw status go report say the port dey denied, while the port still dey answer traffic from internet. You fit read the full mechanism for guide about Docker ports wey dey bypass ufw.
Check wetin really dey listen:
sudo ss -tlnp | grep 8000Healthy result na one line wey bind to 127.0.0.1:8000. If line bind to 0.0.0.0:8000, e mean say the vault dey exposed directly. Correct the mapping, then recreate the container, because port binding dey fixed when dem create the container, and docker compose restart no go change am:
docker compose up -d --force-recreateOne more port still dey show for old guides: 3012, the separate WebSocket port. Vaultwarden remove support for am for version 1.31.0, because notification traffic don move to the main HTTP port. WEBSOCKET_ENABLED and WEBSOCKET_PORT don dey ignored since version 1.29.0. The current switch na ENABLE_WEBSOCKET, wey dey use true by default. If your firewall or compose file still open 3012, close am.
Terminate TLS for reverse proxy, no be for Rocket
Vaultwarden fit serve TLS (transport layer security) by itself through Rocket, wey be the web framework, but the project tell you make you no do am for production. Rocket built-in TLS no get strict SNI (server name indication) support. Na why hardening advice talk say make you reach your instance with hostname, and never use bare IP address. Public IP ranges dey get constant scans, and vault wey dey answer for IP address na vault wey dem go find.
The parts of nginx server block wey matter na these:
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}nginx default client_max_body_size na 1 MB, so if that line no dey, attachment upload go fail with 413 Request Entity Too Large for nginx error log, while Vaultwarden no go log anything. The Upgrade and Connection headers carry WebSocket handshake go /notifications/hub. If you remove dem, vault still go work, but changes no go show for your other devices until you reload the page by hand.
Caddy configuration short pass, and e dey obtain the certificate by itself:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}Then tell Vaultwarden about am:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER already default to X-Real-IP, so the main work na to make sure say proxy really set that header. If e no set am, every log line and every login rate limit go see 127.0.0.1, wey be the proxy itself. This mean say failures from one attacker go count against every user for the instance. Set DOMAIN to the real https URL too, because Vaultwarden dey use am build invitation and password reset links, and WebAuthn security keys dey bind to that origin.
One detail wey people dey miss: WebSocket connection dey pass session token for query string as /notifications/hub?access_token=[JWT]. That token go enter your proxy access log as plain text. Redact the access_token parameter for the log format, or make sure say dem no ship those logs go anywhere wey you no control.
Ban brute force attack for login endpoint
Rate limits dey on by default (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Dem dey slow attacker down. But dem no fit stop am. fail2ban fit stop am, but Vaultwarden first need write log file, and e no dey do that by default:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueFailed login go then produce exactly one line, and na this string your filter need match:
[2026-08-07 09:14:22][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.10. Username: user@example.com.Write the filter go /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =And the jail go /etc/fail2ban/jail.d/vaultwarden.local:
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400If you keep the admin page, add another jail wey get failregex as ^.*Invalid admin token\. IP: <ADDR>.*$, because admin failures dey enter log with different message and the login filter no go ever see dem. Then check your work:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenWorking jail go list your log file under File list and report Currently failed: 0. Type wrong password three times from another network, and that counter go increase. Then the address go show under Banned IP list. If the counter no move, the usual cause na logpath: e must be the file path for the host, no be the /data/... path inside the container. The second usual cause na missing X-Real-IP, wey go make every ban target your own proxy. The rest of the setup, including the SSH jail wey you suppose don dey run, dey for fail2ban guide for Ubuntu 24.04.
Master password still be the whole system
Client-side encryption mean say master password na the key. Short master password for an instance wey attacker don copy the database no get protection from anything for this post, because dem fit attack that copy offline at any speed wey their hardware allow. No server setting fit reach attacker own machine.
PASSWORD_ITERATIONS=600000 easy to misunderstand. E set the iterations for the server-side hash of the login hash wey client dey send, and the project configuration template talk say any changed value go apply to existing users for their next login. E no affect client-side KDF, wey be the setting wey decide how expensive offline attack on the vault go be. That setting belong to each account: person wey register years ago fit still dey use older, lower KDF setting until dem increase am for web vault security settings. That action go re-encrypt their key. Tell dem, because server no fit do am for dem.
Then enable two-factor authentication for each account. E no protect the ciphertext, because vault key come from master password alone. But e stop stolen password from being enough to log in and sync a copy. REQUIRE_DEVICE_EMAIL=true make new-device login email compulsory: if Vaultwarden no fit send am, login go fail, so login from device wey dem never recognise no go happen without notification. E need working SMTP, or every new-device login go dey refused.
Backup na where self-hosted vault dey spoil
A tar czf of the data folder wey you leave for home directory on the same VPS fit cancel every step wey you don do above. That archive hold db.sqlite3 wey contain every user's ciphertext, rsa_key.pem wey fit forge login sessions, and config.json wey get admin token and SMTP password as plaintext. Read access to that one file mean read access to the vault.
Two rules cover am. Move the archive comot from the box. Encrypt am before e comot.
Correctness na another problem. If you copy db.sqlite3 with cp while the service still dey run, you fit get file wey service never finish writing. The file no go open, and you no go know until restore time. Use SQLite own snapshot instead:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"The restore side, wey na the part wey almost nobody dey test, dey covered for the Vaultwarden backup and restore guide.
Wetín You Give Up Compared With Hosted Bitwarden
Make we talk true. Bitwarden hosted service dey run by people wey dem full-time work na to operate am, with published third-party audits and person wey dey on call for 3am. When you self-host, na you go manage how often you patch am.
Vaultwarden dey release security fixes as normal releases, and dem dey come often. As of 7 October 2026, when we check the project release feed, the current release na 1.37.4, wey dem publish on 5 October 2026. The release notes cover seven security advisories. The most serious one get High rating, and dem strongly advise make you update as soon as possible. Na the fourth release since 1.37.0 on 24 July 2026. 1.37.1 follow on 29 July, 1.37.2 on 22 August (e dey required for clients version 2026.8.0 and newer), and 1.37.3 on 13 September. This release revoke 2FA remember tokens when credentials or 2FA change, and e apply rate limits to the prelogin and auth request endpoints. If you run organisations with admins wey you no fully trust, the 1.37.4 notes still suggest make you rotate each organisation API key after update. Any instance wey you set up one year ago and forget still dey run code wey old reach one year. The latest tag no fit help by itself: running container go keep the image wey e start with until you run docker compose pull and recreate am. Put unattended upgrades for Ubuntu in place for the host packages, and put container update for calendar reminder wey you go actually read.
The conclusion wey honest reader suppose draw be say: Bitwarden design na the cryptography for here, and e still hold up, but the operational risk don move completely come you. If you patch am and back am up for another place, Vaultwarden instance for VPS wey you control na reasonable place to keep your passwords. If you no go maintain those two habits, pay for the hosted service and use your attention for another thing. The feature-by-feature comparison dey for Vaultwarden compared with self-hosted Bitwarden.
Harden the host wey dey underneath the container
Vaultwarden na one process for Linux box, and root for that box fit read /vw-data no matter wetin you configure for the application. Run the container as unprivileged user with user: "1000:1000" for your compose file, and make the data folder ownership match am. Mount anything wey the container no dey write to as read only with :ro. Then close the front door: SSH hardening for VPS covers key-only login and disabling password authentication. Na this one stop the simple attack wey fit bypass everything wey you do above.
FAQ
Person fit read my passwords if dem steal the Vaultwarden database?
No be directly. Every vault item dey encrypted for the client with key wey dem derive from the master password, so db.sqlite3 dey contain ciphertext. Wetin dem get immediately na each account email address, the KDF settings, login and device metadata, plus the two-factor secrets for twofactor table. Dem store these secrets unencrypted because the server must calculate the expected code. Dem fit also attack the vault ciphertext offline for as long as dem like. Na why master password length dey decide the result.
Make I use ADMIN_TOKEN or disable the admin page completely?
Disable am if you fit, because most instances need am once to configure SMTP and invite users, and dem no need am again. To disable am, no set ADMIN_TOKEN or DISABLE_ADMIN_TOKEN, remove any "admin_token" key from config.json, then recreate the container. Removing only the environment variable no enough, because settings wey admin page write dey inside config.json and dem get priority. If you keep the page, store the token as an Argon2 hash wey vaultwarden hash produce instead of plaintext random string, and set ADMIN_RATELIMIT_MAX_BURST=3.
My ADMIN_TOKEN correct but /admin dey reject am. Wetin be the problem?
Almost always na $ interpolation. An Argon2 PHC string get several $ characters, and Docker Compose expands dem as variables inside a docker-compose.yml environment: block. So the container receives a mangled value even though your file look correct. Double every $ to $$ for the compose file, or move the value enter an .env file wey you wrap with single quotes. For there, no escaping dey needed. Recreate the container afterwards, because restart no dey pick environment changes.
I still need open port 3012 for notifications?
No. Vaultwarden remove support for WebSocket traffic on port 3012 for version 1.31.0 because notifications move to the main HTTP port. Dem don ignore WEBSOCKET_ENABLED and WEBSOCKET_PORT since 1.29.0. The current setting na ENABLE_WEBSOCKET, and true na the default value. Close 3012 for the firewall and delete am from your compose file. Then make sure your reverse proxy forward the Upgrade and Connection headers, because na dem real-time sync depend on now.