Is Vaultwarden veilig? Zo beveiligt u uw kluis
Vaultwarden versleutelt data op de client, waardoor de server geen plaintext ziet. Leer hoe u risico's zoals een onbeveiligd admin token en onveilige back-ups effectief afdicht.
Is Vaultwarden veilig? Het korte antwoord
Vaultwarden is veilig op het punt dat er het meest toe doet, omdat elk item in de kluis op uw apparaat wordt versleuteld voordat het de server bereikt. De server slaat blobs op die hij niet kan lezen. Iemand die de volledige database kopieert, heeft nog steeds een hoofdwachtwoord nodig om er bruikbare informatie uit te halen.
Dat antwoord is een vereenvoudiging, want de zwakke plekken ontstaan in de configuratie. Een beheerderspaneel achter een raadbaar token. Een containerpoort die is blootgesteld aan het hele internet. Een config.json in platte tekst. Een back-up-tarball die in een thuismap op dezelfde server staat. Geen van deze zaken zijn cryptografische problemen. Ze zijn wel de reden waarom zelfgehoste kluizen worden leeggehaald.
Alles hieronder gaat uit van een werkende installatie. Als u deze nog niet heeft, stel deze dan eerst in met de Vaultwarden installatiehandleiding voor een VPS, en werk daarna deze lijst in volgorde af.
Wat de server daadwerkelijk opslaat
Vaultwarden implementeert het datamodel van Bitwarden. De naam, gebruikersnaam, het wachtwoord, de notities en de URI's van een kluisitem worden in de client versleuteld met een sleutel die is afgeleid van uw hoofdwachtwoord, nog voordat er een verzoek wordt verzonden. De inhoud van bijlagen wordt op dezelfde wijze versleuteld. De server ontvangt onleesbare data met een daaraan gekoppelde UUID (universally unique identifier).
Sommige gegevens zijn geen cijfertekst; u dient precies te weten welke dit zijn:
- Uw e-mailadres voor het account, in leesbare tekst.
- Uw KDF-instellingen (key derivation function) en salt, omdat de client deze nodig heeft om de sleutel bij de volgende aanmelding opnieuw op te bouwen.
- Een server-side hash van de hash van het hoofdwachtwoord die de client verstuurt; deze wordt gebruikt om de aanmelding zelf te verifiëren.
- Metadata: lidmaatschap van organisaties, apparaatnamen, tijdstippen van laatste aanmelding.
- Het geheim voor de tweefactorauthenticatie die de Vaultwarden-aanmelding beveiligt. Dit staat onversleuteld in de
twofactor-tabel, omdat de server de verwachte code moet berekenen om deze met die van u te vergelijken. Dit is niet hetzelfde als een TOTP-geheim (time-based one-time password) dat u binnen een kluisitem opslaat; dat is namelijk wel versleuteld, net als elk ander veld.
De datamap is klein. Bij een Docker-installatie is dit de map die u hebt gekoppeld aan /data.
sudo ls -l /vw-data/db.sqlite3 bevat vrijwel alle statusinformatie. attachments/ bevat geüploade bestanden, één per UUID, en dit is de enige belangrijke gegevenscategorie die niet in databasetabellen staat. sends/ bevat bijlagen van Send en is bedoeld als tijdelijke opslag. icon_cache/ is wegwerpdata. rsa_key.pem en de bijbehorende bestanden ondertekenen de JWT's (JSON web tokens) van aangemelde gebruikers; een kopie van die privésleutel kan dus worden gebruikt om een kluisaanmeldsessie te vervalsen. config.json bestaat pas zodra u de beheerpagina inschakelt, en het project is hierover duidelijk: het bevat het beheertoken en uw SMTP-inloggegevens in leesbare tekst.
Het praktische dreigingsmodel is dus toegang tot het bestandssysteem, niet netwerkcryptografie. Leestoegang tot die ene map geeft toegang tot het e-mailadres van elke gebruiker, hun 2FA-geheimen voor aanmelding, een sleutel om sessies te vervalsen en een offline kopie van elke kluis om op uw gemak aan te vallen. Elke onderstaande stap is bedoeld om onbevoegden uit die map te houden.
Stel eerst het admin-token in
/admin is een volledig configuratiescherm: gebruikerslijst, uitnodigingen, verwijderingen en elke runtime-instelling. Het wordt uitsluitend beveiligd door één gedeeld geheim. Er is geen gebruikersnaam. Er is geen tweefactorauthenticatie per gebruiker.
Oudere handleidingen adviseren om ADMIN_TOKEN te genereren met openssl rand -base64 48. Dat werkt, en het schrijft het geheim in leesbare tekst naar config.json en naar uw compose-bestand. Vaultwarden accepteert echter ook een Argon2 PHC-string (password hashing competition), waardoor de opgeslagen waarde een hash is. Genereer er een via een draaiende container:
docker exec -it vaultwarden /vaultwarden hashOf zonder de draaiende container aan te raken:
docker run --rm -it vaultwarden/server /vaultwarden hashEr wordt tweemaal om een wachtwoord gevraagd, waarna een regel wordt getoond die begint met $argon2id$. Voer bij een bare-metal installatie ./vaultwarden hash uit. Als u liever direct de argon2 CLI gebruikt, documenteert het project de minimale OWASP-parameters:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1Nu de valkuil die veel tijd kost. Een PHC-string bevat veel $-tekens, en Docker Compose behandelt $ als variabele-interpolatie. Als u dit ongeëscaped in een environment:-blok plakt, raakt de waarde die de container bereikt verminkt, waardoor /admin een token afwijst waarvan u weet dat het correct is. Er zijn twee veilige methoden. Verdubbel in docker-compose.yml elk $-teken:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UIIn een .env-bestand is escapen niet nodig, maar gebruik enkele aanhalingstekens:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Beperk vervolgens de toegang tot het paneel en verkort de sessieduur:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20Na drie mislukte pogingen binnen vijf minuten reageert het paneel niet meer op die client. De admin-sessie verloopt na 20 minuten inactiviteit.
Beter nog: schakel de pagina volledig uit. De meeste instanties hebben deze slechts eenmalig nodig om SMTP te configureren en de eerste gebruikers uit te nodigen. Om het uit te schakelen, stelt u noch ADMIN_TOKEN noch DISABLE_ADMIN_TOKEN in, verwijdert u elke "admin_token"-sleutel uit config.json en maakt u de container opnieuw aan. Het verwijderen van de sleutel uit het bestand is essentieel, omdat de admin-pagina instellingen daarheen schrijft en de waarde in config.json voorrang heeft op de omgevingsvariabele. Het enkel verwijderen van de variabele laat de pagina openstaan.
Sluit registratie voordat iemand het domein vindt
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED staat standaard op true. Als u dit zo laat, krijgt iedereen die uw domein bezoekt een account en staan hun gegevens in dezelfde db.sqlite3 als die van u. Stel dit in op false en voeg mensen toe via uitnodigingen vanuit de beheerpagina, waarvoor een werkende SMTP-configuratie vereist is. INVITATIONS_ALLOWED staat standaard ook op true, waardoor organisatie-eigenaren anderen kunnen uitnodigen. Dit is acceptabel als u uw gebruikers vertrouwt, maar op een instance voor één gebruiker hoort dit false te zijn. Als alleen specifieke domeinen zich mogen registreren, is SIGNUPS_DOMAINS_WHITELIST=example.com beperkter dan open registratie, maar aanzienlijk minder veilig dan uitnodigingen.
SHOW_PASSWORD_HINT staat standaard op false en dient daar te blijven. Als deze optie is ingeschakeld, toont het invoeren van een geldig e-mailadres in het inlogformulier de hint voor het hoofdwachtwoord van dat account. Dit lekt zowel de hint als de bevestiging dat het e-mailadres bestaat.
Als uw instance enige tijd met open registratie heeft gedraaid, open dan de beheerpagina en controleer de gebruikerslijst voordat u ervan uitgaat dat u het enige account op de server bent.
De poort die u niet wilde publiceren
De Docker-image luistert op poort 80 binnen de container. Een installatie op bare-metal gebruikt standaard ROCKET_PORT=8000. Het gedocumenteerde run-commando publiceert dit als volgt:
--publish 127.0.0.1:8000:80Het 127.0.0.1:-voorvoegsel is waar het om draait. Schrijf in plaats daarvan -p 8000:80 en Docker bindt aan 0.0.0.0; dit gebeurt door DNAT-regels (destination network address translation) naar de nat-tabel te schrijven. Deze regels worden geëvalueerd vóór de filter-chains die ufw beheert, waardoor ufw status de poort als geweigerd rapporteert terwijl de poort vrolijk reageert op het internet. Het volledige mechanisme is de moeite waard om te lezen in de handleiding over Docker-poorten die ufw omzeilen.
Controleer wat er daadwerkelijk luistert:
sudo ss -tlnp | grep 8000Een gezond resultaat is een enkele regel die gebonden is aan 127.0.0.1:8000. Een regel die gebonden is aan 0.0.0.0:8000 betekent dat de vault direct is blootgesteld. Herstel de mapping en maak de container opnieuw aan, omdat een poortbinding wordt vastgelegd bij het aanmaken van de container en docker compose restart dit niet zal wijzigen:
docker compose up -d --force-recreateIn oude handleidingen komt nog één poort voor: 3012, de aparte WebSocket-poort. Ondersteuning hiervoor is verwijderd in Vaultwarden 1.31.0, omdat notificatieverkeer is verplaatst naar de hoofd-HTTP-poort. WEBSOCKET_ENABLED en WEBSOCKET_PORT worden genegeerd sinds 1.29.0. De huidige switch is ENABLE_WEBSOCKET, die standaard op true staat. Als uw firewall of compose-bestand poort 3012 nog open heeft staan, sluit deze dan.
Beëindig TLS bij een reverse proxy, niet in Rocket
Vaultwarden kan zelf TLS (transport layer security) afhandelen via Rocket, het gebruikte webframework, maar het project raadt dit af voor productieomgevingen. De ingebouwde TLS-functionaliteit van Rocket mist strikte ondersteuning voor SNI (server name indication). Dit is ook de reden waarom het beveiligingsadvies is om uw instantie altijd via een hostnaam te benaderen en nooit via een direct IP-adres. Openbare IP-reeksen worden continu gescand; een vault die reageert op een IP-adres wordt onvermijdelijk gevonden.
De relevante onderdelen van een nginx server block:
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 hanteert standaard client_max_body_size van 1 MB. Zonder deze regel mislukt het uploaden van bijlagen met 413 Request Entity Too Large in het nginx-foutenlogboek, terwijl Vaultwarden zelf niets logt. De headers Upgrade en Connection dragen de WebSocket-handshake over naar /notifications/hub. Laat u deze weg, dan blijft de vault werken, maar verschijnen wijzigingen niet langer automatisch op uw andere apparaten totdat u de pagina handmatig ververst.
Caddy is compacter en verkrijgt het certificaat automatisch:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}Configureer vervolgens Vaultwarden:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER staat standaard al op X-Real-IP, dus de taak is om te garanderen dat de proxy deze header daadwerkelijk meestuurt. Gebeurt dit niet, dan ziet elke logregel en elke rate limit voor inlogpogingen 127.0.0.1, de proxy zelf. Dit betekent dat mislukte inlogpogingen van één aanvaller worden toegerekend aan alle gebruikers van de instantie. Stel DOMAIN ook in op de juiste https URL, omdat Vaultwarden hierop uitnodigings- en wachtwoordherstellinks baseert en WebAuthn-beveiligingssleutels aan deze oorsprong (origin) bindt.
Een detail dat vaak over het hoofd wordt gezien: de WebSocket-verbinding verstuurt het sessietoken in de query string, als /notifications/hub?access_token=[JWT]. Dit komt in leesbare tekst terecht in uw proxy-toegangslogboek. Verberg de parameter access_token in het logformaat, of zorg ervoor dat deze logboeken niet worden verzonden naar locaties waarover u geen controle heeft.
Brute force op het inlog-endpoint blokkeren
Rate limits zijn standaard ingeschakeld (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Deze vertragen een aanvaller, maar stoppen deze niet volledig. fail2ban doet dit wel, maar Vaultwarden moet hiervoor eerst een logbestand schrijven, wat standaard niet gebeurt:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueEen mislukte inlogpoging genereert vervolgens precies één regel, en dit is de string waarop uw filter moet matchen:
[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.Schrijf het filter naar /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =En de jail naar /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 = 14400Als u de admin-pagina heeft behouden, voeg dan een tweede jail toe waarvan de failregex gelijk is aan ^.*Invalid admin token\. IP: <ADDR>.*$, omdat admin-fouten met een ander bericht worden gelogd en het inlogfilter deze anders nooit zal zien. Controleer vervolgens uw werk:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenEen werkende jail vermeldt uw logbestand onder File list en rapporteert Currently failed: 0. Typ drie keer een fout wachtwoord vanaf een ander netwerk; de teller loopt op en het adres verschijnt vervolgens onder Banned IP list. Als de teller niet oploopt, is de gebruikelijke oorzaak logpath: dit moet het bestandspad op de host zijn, niet het /data/...-pad binnen de container. De tweede veelvoorkomende oorzaak is een ontbrekende X-Real-IP, waardoor elke ban uw eigen proxy treft. De rest van de configuratie, inclusief de SSH-jail die u al zou moeten draaien, staat in de fail2ban-handleiding voor Ubuntu 24.04.
Het hoofdwachtwoord blijft de zwakste schakel van het systeem
Client-side encryptie betekent dat het hoofdwachtwoord de sleutel is. Een kort hoofdwachtwoord op een instantie waarvan een aanvaller de database heeft gekopieerd, wordt door niets in dit artikel beschermd, omdat zij die kopie offline aanvallen met de snelheid die hun hardware toelaat. Geen enkele serverinstelling heeft invloed op de eigen machine van een aanvaller.
PASSWORD_ITERATIONS=600000 is het aantal KDF-iteraties dat aan clients wordt doorgegeven wanneer zij een nieuw account aanmaken. Bestaande accounts behouden de waarde waarmee ze zijn aangemaakt; het verhogen van deze waarde verandert dus niets voor gebruikers die zich vorig jaar hebben geregistreerd. Zij moeten dit zelf wijzigen in de beveiligingsinstellingen van de web vault, waardoor hun sleutel opnieuw wordt versleuteld. Informeer hen hierover, aangezien de interface dit niet uit zichzelf zal doen.
Schakel vervolgens tweefactorauthenticatie per account in. Dit beschermt de ciphertext niet, aangezien de vault-sleutel uitsluitend afkomstig is van het hoofdwachtwoord. Het voorkomt echter wel dat een gestolen wachtwoord voldoende is om in te loggen en een kopie te synchroniseren. REQUIRE_DEVICE_EMAIL=true voegt een e-mailbevestigingsstap toe wanneer een account voor het eerst inlogt vanaf een onbekend apparaat.
Backups zijn het punt waar zelfgehoste kluizen vaak de fout in gaan
Een tar czf van de datamap, achtergelaten in een thuismap op dezelfde VPS, maakt elke voorgaande stap ongedaan. Dat archief bevat db.sqlite3 met de ciphertext van elke gebruiker, rsa_key.pem waarmee inlogsessies kunnen worden vervalst, en config.json met het admin-token en het SMTP-wachtwoord in leesbare tekst. Leestoegang tot dat ene bestand betekent leestoegang tot de kluis.
Twee regels zijn hierop van toepassing. Haal het archief van de server af. Versleutel het voordat het de server verlaat.
Er is ook een probleem met de integriteit. Het kopiëren van db.sqlite3 met cp terwijl de service actief is, kan een bestand opleveren dat tijdens het schrijven is onderbroken en niet zal openen; u komt hier pas achter bij het herstel. Gebruik in plaats daarvan de ingebouwde snapshot-functie van SQLite:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"De herstelzijde, het onderdeel dat niemand test, wordt behandeld in de Vaultwarden backup and restore guide.
Wat u opgeeft ten opzichte van de gehoste Bitwarden-dienst
Eerlijkheidshalve: de gehoste dienst van Bitwarden wordt beheerd door mensen die dit als fulltime baan hebben, inclusief gepubliceerde audits door derden en een storingsdienst die om 03:00 uur paraat staat. Bij zelfhosting vervangt u dit door uw eigen patch-ritme.
Vaultwarden brengt beveiligingsupdates uit als reguliere releases. Versie 1.37.0, uitgebracht op 24 juli 2026, is de huidige versie per augustus 2026, en de bijbehorende notities verzoeken gebruikers om zo snel mogelijk bij te werken. Een instantie die u een jaar geleden heeft opgezet en bent vergeten, draait op code van een jaar oud. De latest-tag helpt hier op zichzelf niet bij: een draaiende container behoudt de image waarmee deze is gestart totdat u docker compose pull uitvoert en de container opnieuw aanmaakt. Configureer unattended upgrades op Ubuntu voor de host-pakketten en zet de container-update in een agenda-herinnering die u daadwerkelijk zult lezen.
De conclusie die een eerlijke lezer moet trekken: de cryptografie is het ontwerp van Bitwarden en deze is robuust, terwijl het operationele risico volledig bij u komt te liggen. Als u de software bijwerkt en elders back-ups maakt, is een Vaultwarden-instantie op een VPS die u beheert een verantwoorde plek om uw wachtwoorden op te slaan. Als u deze twee gewoontes niet gaat naleven, betaal dan voor de gehoste dienst en besteed uw aandacht aan andere zaken. De vergelijking per functie vindt u in Vaultwarden vergeleken met zelfgehoste Bitwarden.
De host onder de container beveiligen
Vaultwarden is slechts één proces op een Linux-server, en de root-gebruiker op die server kan /vw-data lezen, ongeacht hoe de applicatie is geconfigureerd. Voer de container uit als een gebruiker zonder privileges met user: "1000:1000" in uw compose-bestand, zorg dat de datamap het juiste eigenaarschap heeft, en koppel alles waar de container niet naar schrijft als alleen-lezen met :ro. Sluit vervolgens de voordeur: SSH-beveiliging op een VPS behandelt inloggen met uitsluitend sleutels en het uitschakelen van wachtwoordauthenticatie; dit is wat de standaardaanvallen stopt die anders alle voorgaande maatregelen zouden omzeilen.
FAQ
Kan iemand mijn wachtwoorden lezen als de Vaultwarden-database wordt gestolen?
Niet direct. Elk item in de kluis wordt in de client versleuteld met een sleutel die is afgeleid van het hoofdwachtwoord, waardoor db.sqlite3 alleen ciphertext bevat. Wat een aanvaller direct in handen krijgt, zijn de e-mailadressen van elk account, de KDF-instellingen, metadata van logins en apparaten, en de two-factor-geheimen in de twofactor-tabel. Deze laatste worden onversleuteld opgeslagen omdat de server de verwachte code moet kunnen berekenen. Een aanvaller kan de ciphertext van de kluis wel onbeperkt offline aanvallen; daarom is de lengte van het hoofdwachtwoord de bepalende factor voor de veiligheid.
Moet ik ADMIN_TOKEN gebruiken of de admin-pagina volledig uitschakelen?
Schakel deze uit indien mogelijk, aangezien de meeste instanties de pagina slechts eenmalig nodig hebben om SMTP te configureren en gebruikers uit te nodigen. Om de pagina uit te schakelen, stelt u noch ADMIN_TOKEN noch DISABLE_ADMIN_TOKEN in, verwijdert u eventuele "admin_token"-sleutels uit config.json en maakt u de container opnieuw aan. Het verwijderen van alleen de omgevingsvariabele is onvoldoende, omdat instellingen die via de admin-pagina zijn opgeslagen in config.json staan en voorrang krijgen. Als u de pagina wel behoudt, sla het token dan op als een Argon2-hash gegenereerd door vaultwarden hash in plaats van als een leesbare tekstreeks, en stel ADMIN_RATELIMIT_MAX_BURST=3 in.
Mijn ADMIN_TOKEN is correct, maar /admin weigert de toegang. Wat is er mis?
Dit wordt bijna altijd veroorzaakt door $-interpolatie. Een Argon2 PHC-string bevat verschillende $-tekens, en Docker Compose interpreteert deze als variabelen binnen een docker-compose.yml environment:-blok. Hierdoor ontvangt de container een verminkte waarde, terwijl het bestand er in uw editor correct uitziet. Verdubbel elk $-teken naar $$ in het compose-bestand, of verplaats de waarde naar een .env-bestand tussen enkele aanhalingstekens, waar geen escaping nodig is. Maak de container daarna opnieuw aan, aangezien wijzigingen in omgevingsvariabelen niet worden verwerkt bij een herstart.
Moet ik poort 3012 nog openstellen voor notificaties?
Nee. Ondersteuning voor WebSocket-verkeer op poort 3012 is verwijderd in Vaultwarden 1.31.0, omdat notificaties zijn verplaatst naar de standaard HTTP-poort. De instellingen WEBSOCKET_ENABLED en WEBSOCKET_PORT worden al genegeerd sinds 1.29.0. De huidige instelling is ENABLE_WEBSOCKET, die standaard op true staat. Sluit poort 3012 in de firewall en verwijder deze uit uw compose-bestand. Zorg er vervolgens voor dat uw reverse proxy de Upgrade- en Connection-headers doorstuurt, aangezien real-time synchronisatie hier nu van afhankelijk is.