Secure ba ang Vaultwarden? Hardening checklist
Naka-encrypt sa client ang bawat vault item, kaya walang plaintext ang server. Suriin ang admin token at backup file: dito karaniwang nanggagaling ang tunay na risk.
Secure ba ang Vaultwarden? Ang maikling sagot
Secure ang Vaultwarden sa pinakamahalagang bahagi, dahil naka-encrypt ang bawat vault item sa device mo bago ito makarating sa server. Nag-iimbak ang server ng mga blob na hindi nito mabasa. Kahit makopya ng isang tao ang buong database, kailangan pa rin niya ang master password para may mapakinabangan siya rito.
Malaki ang saklaw ng sagot na iyan, at ang mga bahaging nasisira ay ang mga bahaging ikaw ang nagko-configure. Isang admin panel na may token na madaling hulaan. Isang container port na naka-publish sa buong internet. Isang plaintext config.json. Isang backup tarball na nakalagay sa home directory sa mismong box. Wala sa mga ito ang problema sa cryptography. Lahat ng ito ang dahilan kung bakit nauubos ang laman ng mga self-hosted vault.
Ipinapalagay ng lahat ng sumusunod na gumagana na ang install. Kung wala ka pa nito, i-set up muna ito gamit ang guide sa pag-install ng Vaultwarden para sa VPS, pagkatapos ay bumalik at sundin ang listahang ito ayon sa pagkakasunod-sunod.
Ano talaga ang iniimbak ng server
Ipinapatupad ng Vaultwarden ang data model ng Bitwarden. Ang pangalan, username, password, notes, at URI ng isang vault item ay ine-encrypt gamit ang key na nagmula sa iyong master password, sa client, bago ipadala ang anumang request. Pareho ring ine-encrypt ang laman ng attachment file. Tumatanggap ang server ng opaque data na may kalakip na UUID (universally unique identifier).
May ilang data na hindi ciphertext, at dapat alam mo kung alin mismo ang mga ito:
- Ang email address ng iyong account, in plaintext.
- Ang iyong KDF (key derivation function) settings at salt, dahil kailangan ito ng client upang buuin muli ang key sa susunod na login.
- Isang server-side hash ng master password hash na ipinapadala ng client, na ginagamit upang i-authenticate mismo ang login.
- Metadata: membership sa organisation, mga device name, at mga oras ng huling login.
- Ang secret para sa two-factor method na nagpoprotekta sa Vaultwarden login. Nasa
twofactortable ito nang hindi naka-encrypt, dahil kailangang kalkulahin ng server ang inaasahang code upang maikumpara sa iyong code. Hindi ito kapareho ng TOTP (time-based one-time password) secret na iniimbak mo sa loob ng vault item, na ine-encrypt tulad ng iba pang field.
Maliit ang data folder. Sa Docker install, ito ang anumang directory na ni-mount mo sa /data.
sudo ls -l /vw-data/Nasa db.sqlite3 ang halos lahat ng state. Nasa attachments/ ang mga uploaded file, tig-isa bawat UUID, at ito lamang ang mahalagang uri ng data na wala sa database tables. Nasa sends/ ang mga Send attachment at nilalayong pansamantala lamang ang mga ito. Maaaring burahin ang icon_cache/. Ang rsa_key.pem at mga kasama nitong file ang pumipirma sa JWTs (JSON web tokens) ng mga naka-login na user, kaya maaaring gamitin ang kopya ng private key na iyon upang pekein ang vault login session. Nagkakaroon lamang ng config.json kapag in-enable mo ang admin page, at malinaw ang babala ng project tungkol dito: naglalaman ito ng admin token at iyong SMTP credentials nang plaintext.
Kaya ang praktikal na threat model ay access sa filesystem, hindi network cryptography. Kapag may read access sa directory na iyon, makukuha ang email address ng bawat user, ang kanilang login 2FA secrets, ang key na maaaring gamitin upang pekein ang mga session, at offline na kopya ng bawat vault na maaaring atakehin anumang oras. Layunin ng bawat hakbang sa ibaba na mapigilan ang ibang tao na makapasok sa directory na iyon.
Unahin ang admin token
/admin ang buong control panel: listahan ng user, mga invitation, deletion, at lahat ng runtime setting. Pinoprotektahan ito ng isang shared secret lamang. Walang username. Walang per-user two-factor authentication.
Sinasabi ng mga lumang guide na i-generate ang ADMIN_TOKEN gamit ang openssl rand -base64 48. Gumagana ito, at isinusulat nito ang secret bilang plaintext sa config.json at sa compose file mo. Tumatanggap din ang Vaultwarden ng Argon2 PHC (password hashing competition) string, kaya hash ang naka-store na value. Mag-generate nito laban sa tumatakbong container:
docker exec -it vaultwarden /vaultwarden hashO nang hindi ginagalaw ang tumatakbong container:
docker run --rm -it vaultwarden/server /vaultwarden hashHihingi ito ng password nang dalawang beses, pagkatapos ay magpi-print ng linyang nagsisimula sa $argon2id$. Sa bare-metal install, patakbuhin ang ./vaultwarden hash. Kung mas gusto mong gamitin nang direkta ang argon2 CLI, idinodokumento ng project ang minimum parameters ng OWASP:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1Narito ang trap na nag-aaksaya ng isang oras. Maraming $ character ang PHC string, at tinatrato ng Docker Compose ang $ bilang variable interpolation. Kapag direktang ipinaste ito nang walang escaping sa isang environment: block, nababago ang value na nakararating sa container, kaya tinatanggihan ng /admin ang token na alam mong tama. May 2 ligtas na paraan. Sa docker-compose.yml, i-double ang bawat $:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UISa .env file, hindi kailangan ang escaping, pero gumamit ng single quotes:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Pagkatapos, lagyan ng rate limit ang panel at paikliin ang session nito:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=203 maling attempt sa loob ng 5 minuto at hindi na sasagot ang panel sa client na iyon. Mag-e-expire ang admin session pagkatapos ng 20 minutong walang aktibidad.
Mas mabuti sa lahat ng ito: i-off ang page. Kailangan ito ng karamihan sa mga instance nang isang beses lamang, para i-configure ang SMTP at imbitahan ang mga unang user, at hindi na ulit gagamitin. Para i-disable ito, huwag magtakda ng ADMIN_TOKEN o DISABLE_ADMIN_TOKEN, alisin ang anumang "admin_token" key sa config.json, pagkatapos ay i-recreate ang container. Mahalaga ang pag-delete ng key sa file dahil doon nagsusulat ng settings ang admin page, at ang nasa config.json ang nangingibabaw sa environment. Kapag variable lang ang inalis, mananatiling bukas ang page.
Isara ang registration bago mahanap ng iba ang domain
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseNaka-default ang SIGNUPS_ALLOWED sa true. Iwanan ito sa ganoong setting at makakagawa ng account ang sinumang makapunta sa domain mo. Mapupunta rin ang kanilang data sa kaparehong db.sqlite3 na ginagamit ng account mo. Itakda ito sa false at magdagdag ng mga user sa pamamagitan ng invitations mula sa admin page. Kailangan nito ng gumaganang SMTP. Naka-default din ang INVITATIONS_ALLOWED sa true at pinapayagan nitong mag-invite ng ibang user ang mga organisation owner. Ayos ito kapag pinagkakatiwalaan mo ang iyong mga user, at dapat itong naka-false sa isang single-user instance. Kung ilang domain lamang ang dapat payagang mag-register, mas limitado ang SIGNUPS_DOMAINS_WHITELIST=example.com kaysa sa open signup at mas mahina ang seguridad nito kumpara sa invitations.
Naka-default ang SHOW_PASSWORD_HINT sa false at dapat itong manatili roon. Kapag naka-enable ito, ang paglalagay ng valid na email address sa login form ay magbabalik ng master password hint ng account na iyon. Nailalabas nito ang hint at kinukumpirma ring umiiral ang email address.
Kung bukas ang signups sa iyong instance sa anumang haba ng panahon, buksan ang admin page at basahin ang user list bago ipalagay na ikaw lamang ang may account dito.
Ang port na hindi mo naman dapat i-publish
Nakikinig ang Docker image sa port 80 sa loob ng container. Naka-default ang bare-metal install sa ROCKET_PORT=8000. Ganito ito ipinapublish ng documented run command:
--publish 127.0.0.1:8000:80Ang prefix na 127.0.0.1: ang buong punto. Kung -p 8000:80 ang isusulat mo, ibinabind ng Docker ang 0.0.0.0, at ginagawa nito iyon sa pamamagitan ng pagsusulat ng DNAT (destination network address translation) rules sa nat table. Ine-evaluate ang mga rule na iyon bago ang filter chains na mina-manage ng ufw. Kaya iniuulat ng ufw status na denied ang port kahit maayos itong sumasagot sa internet. Mahahalagang basahin ang buong mekanismo sa gabay sa pag-bypass ng Docker ports sa ufw.
Suriin kung ano talaga ang nakikinig:
sudo ss -tlnp | grep 8000Ang tamang resulta ay isang linyang naka-bind sa 127.0.0.1:8000. Kapag naka-bind ang isang linya sa 0.0.0.0:8000, direktang exposed ang vault. Ayusin ang mapping, pagkatapos ay i-recreate ang container, dahil naka-fix ang port binding kapag ginagawa ang container at hindi ito babaguhin ng docker compose restart:
docker compose up -d --force-recreateMay isa pang port na lumalabas sa mga lumang guide: 3012, ang hiwalay na WebSocket port. Inalis ang support para rito sa Vaultwarden 1.31.0 dahil inilipat ang notification traffic sa pangunahing HTTP port. Hindi na ginagamit ang WEBSOCKET_ENABLED at WEBSOCKET_PORT mula pa noong 1.29.0. Ang kasalukuyang switch ay ENABLE_WEBSOCKET, na naka-default sa true. Kung binubuksan pa rin ng firewall o compose file mo ang 3012, isara ito.
I-terminate ang TLS sa reverse proxy, hindi sa Rocket
Maaaring mag-serve ang Vaultwarden ng TLS (transport layer security) mismo sa pamamagitan ng Rocket, ang web framework nito, pero sinasabi ng project na huwag itong gawin sa production. Walang mahigpit na suporta sa SNI (server name indication) ang built-in TLS ng Rocket. Ito rin ang dahilan kung bakit ipinapayo ng hardening guidance na hostname ang gamitin sa pag-access sa instance at huwag kailanman bare IP address. Patuloy na ini-scan ang mga public IP range, at ang vault na sumasagot sa isang IP address ay madaling matuklasan.
Ito ang mahahalagang bahagi ng server block ng nginx:
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;
}Bilang default, itinatakda ng nginx ang client_max_body_size sa 1 MB. Kung wala ang linyang iyon, mabibigo ang attachment upload at lalabas ang 413 Request Entity Too Large sa nginx error log, habang walang anumang itinatala ang Vaultwarden. Ipinapasa ng Upgrade at Connection headers ang WebSocket handshake sa /notifications/hub. Kapag inalis ang mga ito, gagana pa rin ang vault, pero hindi na lalabas ang mga pagbabago sa iba mong device hangga't hindi mo mano-manong nire-reload ang page.
Mas maikli ang configuration sa Caddy, at awtomatiko nitong kinukuha ang certificate:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}Pagkatapos, ipaalam ito sa Vaultwarden:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPAng IP_HEADER ay may default nang X-Real-IP. Kaya ang kailangang tiyakin ay aktuwal na itinatakda ng proxy ang header na iyon. Kung hindi, makikita ng bawat log line at bawat login rate limit ang 127.0.0.1, ang proxy mismo. Dahil dito, mabibilang ang mga failed attempt ng isang attacker laban sa lahat ng user sa instance. Itakda rin ang DOMAIN sa totoong https URL, dahil ginagamit ito ng Vaultwarden sa pagbuo ng invitation at password reset links. Naka-bind din dito ang origin ng WebAuthn security keys.
May isang detalyeng madalas hindi napapansin: ipinapasa ng WebSocket connection ang session token sa query string bilang /notifications/hub?access_token=[JWT]. Dahil dito, malinaw na lumalabas ito sa proxy access log. I-redact ang access_token parameter sa log format, o tiyaking hindi ipinapadala ang mga log na iyon sa anumang system na wala kang kontrol.
I-block ang brute force sa login endpoint
Naka-enable bilang default ang rate limits (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Pinapabagal nito ang attacker. Hindi nito siya tuluyang pinipigilan. Ang fail2ban ang gumagawa nito, pero kailangang magsulat muna ang Vaultwarden sa isang log file, at hindi ito ginagawa ng Vaultwarden bilang default:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueAng isang nabigong login ay maglalabas ng eksaktong isang linya. Ito ang string na kailangang i-match ng filter mo:
[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.Isulat ang filter sa /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =At ang jail sa /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 = 14400Kung pinanatili mo ang admin page, magdagdag ng pangalawang jail na ang failregex ay ^.*Invalid admin token\. IP: <ADDR>.*$, dahil ibang message ang ginagamit sa pag-log ng mga nabigong admin login at hindi kailanman makikita ng login filter ang mga ito. Pagkatapos, i-check ang configuration:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenAng gumaganang jail ay naglilista ng log file mo sa ilalim ng File list at nag-uulat ng Currently failed: 0. Mag-type ng maling password nang tatlong beses mula sa ibang network at tataas ang counter na iyon. Pagkatapos, lilitaw ang address sa ilalim ng Banned IP list. Kung hindi gumagalaw ang counter, ang karaniwang sanhi ay logpath: dapat itong path ng file sa host, hindi ang /data/... path sa loob ng container. Ang ikalawang karaniwang sanhi ay nawawalang X-Real-IP, na nagiging sanhi upang ang sarili mong proxy ang maging target ng bawat ban. Ang iba pang bahagi ng setup, kabilang ang SSH jail na dapat ay tumatakbo na, ay nasa fail2ban guide para sa Ubuntu 24.04.
Ang master password pa rin ang nagpoprotekta sa buong system
Sa client-side encryption, ang master password ang nagsisilbing key. Kung maikli ang master password sa isang instance na nakopya na ng attacker ang database, walang proteksyong inilalarawan sa post na ito ang makapipigil sa pag-atake sa kopyang iyon offline, sa bilis na kaya ng hardware ng attacker. Walang server setting na makaaabot sa sariling machine ng attacker.
PASSWORD_ITERATIONS=600000 ang KDF iteration count na ipinapasa sa mga client kapag gumagawa sila ng bagong account. Ang mga kasalukuyang account ay nananatili sa halagang ginamit noong ginawa ang mga ito. Kaya walang mababago sa mga user na nag-sign up noong nakaraang taon kung itataas mo ito. Kailangan nila itong baguhin mismo sa security settings ng web vault. Sa prosesong ito, muling ie-encrypt ang kanilang key. Sabihin ito sa kanila, dahil walang lalabas na abiso tungkol dito sa interface.
Pagkatapos, i-enable ang two-factor authentication para sa bawat account. Hindi nito pinoprotektahan ang ciphertext, dahil ang vault key ay nagmumula lamang sa master password. Pinipigilan nitong maging sapat ang nakaw na password para makapag-login at makapag-sync ng kopya. REQUIRE_DEVICE_EMAIL=true ay nagdaragdag ng email confirmation step sa unang pag-login ng account mula sa hindi nakilalang device.
Dito nagkakamali ang mga backup ng self-hosted vault
Binalewalang halaga ng isang tar czf ng data folder na naiwan sa home directory sa parehong VPS ang lahat ng hakbang sa itaas. Nasa archive na iyon ang db.sqlite3 kasama ang ciphertext ng bawat user, rsa_key.pem na maaaring gamitin sa pag-forge ng login session, at config.json na naglalaman nang plaintext ng admin token at SMTP password. Ang read access sa file na iyon ay read access sa vault.
Dalawang rule ang sumasaklaw dito. Ilabas ang archive sa server. I-encrypt ito bago ito ilipat.
May problema rin sa correctness. Ang pagkopya sa db.sqlite3 gamit ang cp habang tumatakbo ang service ay maaaring lumikha ng file na kasalukuyang sinusulatan at hindi mabubuksan. Malalaman mo lamang ito kapag nag-restore ka na. Gamitin ang sariling snapshot ng SQLite:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"Sinasaklaw ng restore side, na hindi sinusubok ng karamihan, ang backup at restore guide ng Vaultwarden.
Ano ang isinusuko mo kumpara sa hosted Bitwarden
Tapat na pagtaya. Ang hosted service ng Bitwarden ay pinapatakbo ng mga taong full-time ang trabaho sa pag-operate nito. May mga inilalathalang audit mula sa third party, at may taong naka-on-call ganap na alas-3 ng umaga. Kapag nag-self-host ka, ipinagpapalit mo iyon sa sarili mong patch cadence.
Naglalabas ang Vaultwarden ng mga security fix bilang mga ordinaryong release. Ang Version 1.37.0, na inilabas noong 24 July 2026, ang kasalukuyang version noong August 2026. Nakasaad sa release notes nito na dapat mag-update ang mga user sa lalong madaling panahon. Ang instance na sine-setup mo noong isang taon at nakalimutan mo na ay nagpapatakbo ng code na isang taon nang luma. Hindi sapat ang latest tag: nananatili sa tumatakbong container ang image kung saan ito nagsimula hanggang patakbuhin mo ang docker compose pull at i-recreate ito. Magpatupad ng unattended upgrades sa Ubuntu para sa host packages, at magtakda ng calendar reminder para sa container update na talagang mababasa mo.
Ang konklusyong dapat mabuo ng isang tapat na mambabasa: maayos ang cryptography dahil nakabatay ito sa design ng Bitwarden, ngunit ganap na napupunta sa iyo ang operational risk. Kung magpapatupad ka ng patches at magba-back up sa ibang lokasyon, makatuwirang lugar ang Vaultwarden instance sa isang VPS na kontrolado mo para sa pag-iimbak ng iyong mga password. Kung hindi mo magagawa ang dalawang gawi na iyon, magbayad para sa hosted service at ilaan ang iyong oras sa ibang bagay. Makikita ang paghahambing ng bawat feature sa Paghahambing ng Vaultwarden at self-hosted Bitwarden.
I-harden ang host sa ilalim ng container
Ang Vaultwarden ay isang process sa isang Linux box, at maaaring basahin ng root sa box na iyon ang /vw-data anuman ang configuration ng application. Patakbuhin ang container bilang unprivileged user gamit ang user: "1000:1000" sa compose file, tiyaking pagmamay-ari ng katugmang user ang data folder, at i-mount bilang read-only gamit ang :ro ang anumang hindi kailangang sulatan ng container. Pagkatapos, isara ang front door: saklaw ng Pag-harden ng SSH sa isang VPS ang key-only login at pag-disable ng password authentication. Ito ang pumipigil sa karaniwang attack na lumalampas sa lahat ng nabanggit.
FAQ
Maaari bang mabasa ng iba ang aking mga password kapag ninakaw nila ang Vaultwarden database?
Hindi kaagad. Ang bawat vault item ay ine-encrypt sa client gamit ang key na nakuha mula sa master password, kaya ang db.sqlite3 ay naglalaman ng ciphertext. Ang makukuha nila kaagad ay ang email address ng bawat account, ang KDF settings, login at device metadata, at ang two-factor secrets sa twofactor table. Naka-store ang mga ito nang walang encryption dahil kailangang i-compute ng server ang inaasahang code. Maaari rin nilang atakehin offline ang vault ciphertext hangga't gusto nila. Dahil dito, ang haba ng master password ang pangunahing nagtatakda ng resulta.
Dapat ko bang gamitin ang ADMIN_TOKEN o tuluyang i-disable ang admin page?
I-disable ito kung maaari, dahil karamihan ng instance ay isang beses lang ito kailangan para i-configure ang SMTP at mag-invite ng users. Pagkatapos nito, hindi na ito ginagamit. Para i-disable ito, huwag itakda ang ADMIN_TOKEN o DISABLE_ADMIN_TOKEN, alisin ang anumang "admin_token" key mula sa config.json, at pagkatapos ay i-recreate ang container. Hindi sapat ang pag-alis lang ng environment variable, dahil ang mga setting na isinulat ng admin page ay naka-store sa config.json at may precedence. Kung pananatilihin mo ang page, i-store ang token bilang Argon2 hash na ginawa ng vaultwarden hash sa halip na plaintext random string, at itakda ang ADMIN_RATELIMIT_MAX_BURST=3.
Tama ang aking ADMIN_TOKEN pero tinatanggihan ito ng /admin. Ano ang mali?
Halos palagi itong dahil sa $ interpolation. Naglalaman ang Argon2 PHC string ng ilang $ character. Ine-expand ng Docker Compose ang mga ito bilang variables sa loob ng docker-compose.yml environment: block, kaya nakakatanggap ang container ng maling value kahit tama ang nakikita sa file. Gawing $$ ang bawat $ sa compose file, o ilipat ang value sa isang .env file na nakabalot sa single quotes. Sa ganitong paraan, hindi kailangan ng escaping. I-recreate ang container pagkatapos, dahil hindi nakukuha ng restart ang mga pagbabago sa environment.
Kailangan ko pa bang buksan ang port 3012 para sa notifications?
Hindi. Inalis ang suporta sa WebSocket traffic sa port 3012 sa Vaultwarden 1.31.0 dahil inilipat ang notifications sa pangunahing HTTP port. Hindi na rin ginagamit ang WEBSOCKET_ENABLED at WEBSOCKET_PORT mula pa noong 1.29.0. Ang kasalukuyang setting ay ENABLE_WEBSOCKET, na true bilang default. Isara ang 3012 sa firewall at alisin ito sa compose file. Tiyakin ding ipinapasa ng reverse proxy ang Upgrade at Connection headers, dahil dito na nakadepende ang aktuwal na real-time sync.