Vaultwarden sur VPS : héberger vos mots de passe
Hébergez un gestionnaire de mots de passe compatible Bitwarden avec Vaultwarden et Docker sur VPS : HTTPS d'abord, jeton admin, Fail2ban, sauvegardes testées.
Ce que vous allez construire
Un gestionnaire de mots de passe qui vous appartient entièrement : Vaultwarden tourne dans un seul petit conteneur derrière un proxy inverse qui termine le HTTPS, avec les applications officielles Bitwarden sur votre téléphone, votre ordinateur portable et votre navigateur pointées vers lui. Vaultwarden réimplémente l'API du serveur Bitwarden en Rust et parle le même protocole que bitwarden.com, donc chaque client officiel fonctionne sans modification, mais il tient dans environ 100 Mo de RAM au lieu de la pile officielle multi-conteneurs.
L'installation elle-même tient en une douzaine de lignes de Compose. Les trois choses qui comptent vraiment, et qui cassent, sont celles-ci : le TLS doit exister avant même que vous ne chargiez le coffre web, les inscriptions publiques doivent être fermées dès que votre propre compte existe, et le volume de données doit être sauvegardé puis restauré à titre de test, car ce seul répertoire contient tous les mots de passe que vous possédez.
Prérequis et les pièges à connaître
- Un VPS avec Docker Engine et le plugin Compose, sur une machine KVM Ubuntu 24.04 fraîche avec root ou sudo. 512 Mo de RAM suffisent vraiment ; 1 Go est confortable. C'est l'une des choses les plus légères que vous puissiez faire tourner, elle figure en haut de la liste des services qui valent la peine d'être auto-hébergés.
- Un domaine avec un enregistrement A (et AAAA si vous avez l'IPv6) qui pointe
vault.example.comvers le VPS. Le certificat TLS est émis pour ce nom exact, donc le DNS doit se résoudre avant de commencer. - Les ports 80 et 443 ouverts sur internet, terminés par votre proxy inverse, jamais par Vaultwarden directement. Le port 80 sert uniquement au défi de certificat ACME et à une redirection HTTP vers HTTPS.
- Le plus gros piège d'entrée : les clients Bitwarden refusent de parler à un serveur qui n'est pas en HTTPS. Il n'y a pas de « testez-le d'abord en http », ce chemin ne fonctionne pas, pour une raison concrète expliquée juste après.
Pourquoi Vaultwarden, et non la pile officielle Bitwarden
Les mêmes clients, pour une fraction du poids. Le Bitwarden auto-hébergé officiel se présente comme un ensemble de conteneurs (MSSQL, Nginx, Identity, Api, Admin et d'autres) et réclame environ 2 Go de RAM. Vaultwarden est un seul binaire qui stocke tout dans une base de données SQLite par défaut et reste au repos à quelques dizaines de mégaoctets. Pour une personne seule, une famille ou une petite équipe, c'est le choix évident, et comme il implémente fidèlement l'API Bitwarden, vos données restent portables entre lui et bitwarden.com.
Ce que vous abandonnez, c'est l'essentiel de la surface entreprise : pas de provisionnement SCIM (même si le SSO OpenID Connect expérimental est arrivé en 1.35.0), et vous êtes l'opérateur, donc les correctifs, le HTTPS et les sauvegardes sont votre travail. Ce guide, ce sont ces trois travaux.
Pourquoi le HTTPS n'est pas optionnel
Le coffre web Bitwarden et les extensions de navigateur dérivent vos clés de chiffrement dans le navigateur à l'aide de l'API Web Crypto (window.crypto.subtle). Les navigateurs n'exposent crypto.subtle que dans un contexte sécurisé, en HTTPS, ou dans le cas particulier de http://localhost. En simple http://vault.example.com, il vaut undefined, donc dès que l'application dérive une clé, elle lève une erreur, et la console affiche :
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')La page se fige ou affiche une erreur de chiffrement générique, et rien ne se connecte. Les clients de bureau, mobile et navigateur exécutent leur propre vérification vis-à-vis d'une URL auto-hébergée, et face à un point de terminaison http (ou injoignable), ils refusent avec :
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.Les deux ont la même cause : pas de HTTPS valide. Nous mettons donc en place le TLS d'abord et n'ouvrons jamais le coffre en http, pas même une seule fois pour un coup d'œil rapide.
Étape 1 : DNS et le proxy inverse (le TLS d'abord)
Faites pointer l'enregistrement vers votre VPS et confirmez qu'il se résout vers la bonne adresse :
dig +short vault.example.comLa ligne qu'il affiche doit être l'IP de votre VPS. Si elle est vide ou fausse, corrigez le DNS et attendez la fin du TTL, car l'émission du certificat échoue face à un nom qui ne se résout pas.
Pour la façade HTTPS, ce guide utilise Traefik, qui émet et renouvelle automatiquement les certificats Let's Encrypt et s'intègre directement à Compose. Si vous ne l'exécutez pas déjà, suivez d'abord la mise en place du proxy inverse Traefik et du TLS automatique ; il crée un réseau Docker externe (proxy ci-dessous) et un résolveur ACME (letsencrypt) auquel le service Vaultwarden se rattache. Un simple nginx avec un certificat émis à la main fonctionne de façon identique du côté de Vaultwarden.
Vous préférez nginx et Certbot plutôt que Traefik ? Placez Vaultwarden sur 127.0.0.1:8080 (ajoutez ports: ["127.0.0.1:8080:80"] au service et retirez les labels Traefik), puis émettez un certificat et faites un proxy vers lui. La partie certificat est couverte dans l'émission de certificats Let's Encrypt avec Certbot et nginx. L'élément critique en plus est la mise à niveau WebSocket sur le chemin des notifications :
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Notez la ligne X-Real-IP, c'est ce qui permet plus tard à Fail2ban de voir le véritable attaquant au lieu de 127.0.0.1. Tout le reste de ce guide est identique, que ce soit Traefik ou nginx qui se trouve en façade.
Étape 2 : le fichier Compose
Créez d'abord le répertoire du projet. Ce guide utilise /opt/vaultwarden, ce qui rend prévisible le nom du projet Compose, et donc le volume de données, vaultwarden_vw-data ; les étapes Fail2ban et sauvegarde ci-dessous dépendent de ce nom exact.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCréez un .env pour le secret admin et le fichier Compose dans ce répertoire.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGénérez ce jeton avec openssl rand -base64 48 et collez-le. (Une forme hachée plus robuste est expliquée juste après ; une longue chaîne aléatoire convient pour débuter.)
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: trueDeux choses dans ce fichier portent toute la conception. Il n'y a aucune correspondance ports:, donc Vaultwarden n'est accessible qu'à travers Traefik et son TLS ; publier son port sur l'hôte, c'est ainsi que les gens servent le coffre en http par accident. Et DOMAIN doit être l'URL HTTPS publique complète : elle est intégrée dans les liens de pièces jointes, la 2FA WebAuthn et le point de terminaison des notifications, donc une valeur fausse ou en http les casse même quand le site se charge. Le tag latest est une exception délibérée à la règle habituelle du jamais-latest : Vaultwarden livre ses versions stables sous la forme d'une seule image glissante, avec :testing comme canal de pré-version distinct, donc mettez à jour de façon volontaire et parcourez les notes de version avant de tirer l'image.
Lancez-le et surveillez le journal :
docker compose up -d
docker compose logs -f vaultwardenUn démarrage correct se termine par une ligne du type Rocket has launched from http://0.0.0.0:80. Laissez à Traefik quelques secondes pour récupérer le certificat, puis chargez https://vault.example.com ; vous devriez obtenir le coffre web Bitwarden avec un cadenas valide et aucun avertissement de certificat.
Étape 3 : un ADMIN_TOKEN robuste, et le piège des $$
ADMIN_TOKEN protège /admin, le panneau qui peut lire chaque utilisateur et chaque réglage de votre instance, alors traitez-le comme un mot de passe root. Deux formes fonctionnent.
La forme simple est la chaîne aléatoire que vous avez déjà générée avec openssl rand -base64 48. Comme le base64 ne contient jamais de $, elle se place directement dans .env sans échappement.
La forme renforcée est un hachage Argon2 au format PHC, de sorte que le jeton en clair n'est jamais stocké sur le disque. Générez-en un à partir de la même image :
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspIl demande deux fois puis affiche une chaîne commençant par $argon2id$v=19$.... Voici le piège qui coûte une heure aux gens : Docker Compose traite $ comme une interpolation de variable, vous devez donc doubler chaque $ en $$ quand vous collez le hachage dans le fichier Compose. Placez-le directement sous environment:, pas via .env, et ne l'entourez pas de guillemets :
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGSi vous laissez les signes $ simples, Compose avertit The "argon2id" variable is not set et vide le jeton, et /admin rejette alors votre mot de passe pourtant correct. Exécutez docker compose up -d, et conservez le texte en clair que vous avez saisi à l'invite dans votre propre gestionnaire de mots de passe.
Étape 4 : créez votre compte, puis verrouillez la porte
Avec SIGNUPS_ALLOWED: "true", ouvrez https://vault.example.com, cliquez sur Create account, et inscrivez-vous avec votre e-mail et un mot de passe maître robuste. Ce mot de passe maître n'est jamais récupérable, il n'y a aucune réinitialisation, alors stockez-le d'abord dans un endroit durable.
Maintenant, fermez la porte. Modifiez le fichier Compose pour désactiver les inscriptions :
SIGNUPS_ALLOWED: "false"Réappliquez avec docker compose up -d. Ce n'est pas un durcissement que vous pouvez remettre à plus tard. Laissée ouverte, toute personne qui trouve l'URL, et les robots d'indexation la trouvent, peut créer un compte sur votre serveur. Ils ne peuvent pas lire votre coffre, mais ils consomment des ressources et transforment votre instance privée en service ouvert. Le signe que vous l'avez laissée activée : /admin liste des comptes que vous n'avez jamais créés.
Pour ajouter plus tard des proches ou des collègues sans rouvrir les inscriptions publiques, utilisez le bouton Invite User dans /admin ; ce chemin nécessite un SMTP configuré pour que l'invité reçoive son lien.
Étape 5 : accéder à /admin
Rendez-vous sur https://vault.example.com/admin et saisissez le jeton admin en clair (la chaîne aléatoire, ou le mot de passe que vous avez haché, pas le hachage lui-même). À l'intérieur, vous pouvez lister les utilisateurs, ajuster les réglages, envoyer un e-mail de test et prendre un instantané de la base de données.
Si la page renvoie 404 Not Found, c'est que ADMIN_TOKEN est vide ou non défini, ce qui désactive entièrement le panneau, un choix valable en soi si vous n'en avez jamais besoin. Si elle se charge mais rejette votre jeton, voyez le piège d'échappement des $$ dans la liste des pannes ci-dessous. Vous avez oublié le jeton ? Il n'y a aucune invite de récupération ; modifiez .env ou le fichier Compose, définissez-en un nouveau, puis docker compose up -d.
Étape 6 : connectez les clients Bitwarden
Chaque client officiel peut pointer vers un serveur auto-hébergé, alors installez le client Bitwarden de bureau, mobile ou navigateur depuis les boutiques habituelles ; vous n'avez pas besoin d'une version spéciale Vaultwarden.
Avant de vous connecter, ouvrez la roue des réglages sur l'écran de connexion (intitulée Self-hosted ou Region → Self-hosted), réglez Server URL sur https://vault.example.com, et enregistrez. Puis connectez-vous avec l'e-mail et le mot de passe maître que vous avez enregistrés ; le client devrait se connecter immédiatement et proposer de remplir et d'enregistrer vos identifiants.
Si un client affiche This is not a recognized Bitwarden server. You may need to check with your provider or update your server., c'est que l'URL est fausse, utilise http, ou que le certificat n'est pas de confiance ; vérifiez d'abord que https://vault.example.com se charge proprement dans un navigateur. Les mises à jour lentes sur les autres appareils relèvent du push WebSocket, traité plus bas.
Étape 7 : une jail Fail2ban pour le point de connexion
Vaultwarden journalise chaque échec de connexion dans le fichier défini par LOG_FILE, exactement ce dont a besoin une protection contre la force brute. Si vous n'exécutez pas déjà Fail2ban, l'installation et les bases sont dans le guide de durcissement SSH avec Fail2ban ; ici, nous ajoutons une jail pour le coffre.
Trouvez d'abord où réside le volume nommé sur l'hôte, pour que Fail2ban puisse lire le journal :
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'Cela affiche quelque chose comme /var/lib/docker/volumes/vaultwarden_vw-data/_data ; le journal est vaultwarden.log à l'intérieur. Créez le filtre :
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Et la jail :
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600Rechargez avec sudo systemctl restart fail2ban et confirmez avec sudo fail2ban-client status vaultwarden.
Trois détails Docker déterminent si cela protège quoi que ce soit. Premièrement, si le journal affiche IP: 127.0.0.1 ou l'adresse de votre proxy à chaque tentative échouée, Vaultwarden bannit le proxy ; réglez IP_HEADER sur l'en-tête que votre proxy envoie réellement (X-Forwarded-For pour Traefik, X-Real-IP pour le bloc nginx ci-dessus, CF-Connecting-IP derrière Cloudflare). Deuxièmement, la bonne chaîne iptables dépend de votre proxy : avec Traefik exécuté comme conteneur avec des ports publiés, le trafic traverse le chemin FORWARD de Docker, donc le bannissement doit se trouver dans DOCKER-USER comme ci-dessus ; mais si vous avez choisi l'option nginx sur l'hôte de l'étape 1, les connexions se terminent sur nginx dans la chaîne INPUT de l'hôte et un bannissement DOCKER-USER ne les voit jamais ; dans ce cas, supprimez la ligne chain = DOCKER-USER pour que Fail2ban utilise la chaîne INPUT par défaut. Troisièmement, utilisez banaction = iptables-allports plutôt que la valeur par défaut basée sur le port ; cette jail ne définit aucun port, et un bannissement tous ports dans DOCKER-USER bloque proprement le fautif de chaque service publié sur la machine.
Étape 8 : sauvegardez le coffre, puis restaurez-le vraiment
Le volume vw-data est votre gestionnaire de mots de passe. Il contient db.sqlite3 (chaque entrée), les répertoires attachments/ et sends/, les fichiers rsa_key.* qui signent les sessions de connexion, et config.json du panneau d'administration. Une sauvegarde qui en oublie l'un échoue au moment où vous en avez besoin.
Copier db.sqlite3 pendant que Vaultwarden écrit peut capturer un fichier à moitié écrit et corrompu, alors prenez un instantané à froid ; l'indisponibilité est de quelques secondes :
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwardenExécutez-le depuis cron chaque nuit et copiez le .tgz hors de la machine ; une sauvegarde qui ne vit que sur le serveur que vous protégez n'est pas une sauvegarde. La façon propre de l'expédier est une sauvegarde restic nocturne vers un autre serveur ou un stockage objet, qui chiffre l'archive et déduplique pour vous les instantanés répétés. Le bouton Backup Database du panneau d'administration est un instantané à chaud pratique du seul fichier SQLite, mais il omet les pièces jointes et les clés.
Maintenant, le rituel qui distingue une vraie sauvegarde d'une sauvegarde pleine d'espoir : restaurez-la une fois et prouvez qu'elle fonctionne :
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/serverDepuis votre ordinateur portable, créez un tunnel avec ssh -L 8888:127.0.0.1:8888 you@your-vps et ouvrez http://localhost:8888. Comme localhost est un contexte sécurisé, crypto.subtle est disponible et le coffre se déchiffre ici en simple http, le seul endroit où c'est autorisé. Connectez-vous avec votre mot de passe maître et confirmez que vos entrées sont présentes : si elles le sont, votre base de données, vos clés RSA et votre mot de passe maître font tous l'aller-retour, et vous pouvez reconstruire sur un VPS neuf en quelques minutes. Arrêtez le conteneur avec Ctrl-C et supprimez /tmp/vw-restore.
Modes de défaillance, avec les messages que vous verrez
Cannot read properties of undefined (reading 'importKey') dans la console du navigateur. Le coffre a été chargé en http, donc crypto.subtle est indéfini ; accédez-y uniquement via https:// et ajoutez la redirection HTTP vers HTTPS au niveau du proxy.
This is not a recognized Bitwarden server... dans un client. L'URL du serveur est en http, mal saisie, ou le certificat n'est pas de confiance ; confirmez que https://vault.example.com affiche un cadenas valide, puis saisissez-la de nouveau dans les réglages auto-hébergés du client.
/admin rejette le mot de passe pourtant correct. Le hachage Argon2 a perdu son échappement, chaque $ doit être $$ dans Compose, ou vous avez saisi le hachage au lieu du texte en clair qu'il représente.
Synchronisation lente entre appareils ; la console affiche WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. Le proxy ne transmet pas les en-têtes Upgrade/Connection ; Traefik le fait automatiquement, nginx a besoin des deux lignes de mise à niveau de l'étape 1. Le coffre fonctionne encore, il ne se synchronise qu'à l'ouverture. L'ancien port dédié 3012 a disparu depuis la v1.31.0, aucune route WebSocket distincte n'est donc nécessaire.
Fail2ban signale un bannissement mais l'attaquant continue de se connecter. Il bannit 127.0.0.1 parce que IP_HEADER est erroné, ou le bannissement se trouve dans la mauvaise chaîne iptables ; réglez chain = DOCKER-USER et banaction = iptables-allports.
Mises à jour
Tirez la nouvelle image et recréez ; le volume nommé et toutes vos données persistent :
docker compose pull
docker compose up -dVaultwarden publie des versions fréquentes. Surveillez les notes de version du projet plutôt que d'épingler une version corrective, car certaines versions comportent des notes de migration. Prenez une nouvelle sauvegarde avant toute montée de version majeure ; vous pouvez revenir en arrière en restaurant l'archive tar dans un nouveau volume.
FAQ
Vaultwarden est-il la même chose que Bitwarden ?
C'est un serveur compatible et indépendant, pas le serveur officiel. Vaultwarden réimplémente l'API du serveur Bitwarden en Rust, donc les clients officiels de bureau, mobile, navigateur et CLI fonctionnent tous avec lui, pour une fraction des ressources de la pile officielle. Le format du coffre est le même, vous pouvez donc migrer dans les deux sens en exportant et en important.
Ai-je vraiment besoin de HTTPS, ou puis-je l'exécuter en http sur mon LAN ?
Vous avez besoin de HTTPS pour tout sauf un test en localhost. Le coffre web Bitwarden et les extensions utilisent l'API Web Crypto du navigateur, qui n'est disponible que dans un contexte sécurisé, donc en simple http le client lève Cannot read properties of undefined et ne se connecte jamais. La seule adresse http qui fonctionne est http://localhost, ce qui explique pourquoi le test de restauration de l'étape 8 utilise un tunnel SSH.
Comment empêcher des inconnus de s'inscrire sur mon serveur ?
Réglez SIGNUPS_ALLOWED: "false" dans le fichier Compose et exécutez docker compose up -d, immédiatement après avoir créé votre propre compte. À partir de là, ajoutez de nouvelles personnes via le bouton Invite User dans /admin, qui nécessite un SMTP configuré pour qu'elles reçoivent le lien d'invitation. Vérifiez de temps en temps la liste des utilisateurs admin pour confirmer qu'aucun compte inattendu n'est apparu.
Comment sauvegarder mon coffre Vaultwarden ?
Arrêtez brièvement le conteneur et archivez tout le volume vw-data, db.sqlite3, attachments/, sends/, config.json et les fichiers rsa_key.*, puis copiez l'archive hors du serveur, idéalement via un cron nocturne. Copier le fichier SQLite en cours d'utilisation pendant que le serveur tourne risque un instantané corrompu, alors prenez-le à froid. Plus important encore, restaurez-la une fois dans un conteneur jetable et connectez-vous, pour savoir que la sauvegarde est réelle avant d'en dépendre.
Est-il réellement sûr d'auto-héberger mes mots de passe ?
Oui, quand vous faites les trois choses que ce guide couvre : un vrai HTTPS, des inscriptions fermées plus un jeton admin robuste, et des sauvegardes testées. Votre coffre est chiffré côté client avec votre mot de passe maître, donc même le serveur ne voit jamais vos mots de passe en clair ; un db.sqlite3 volé est inutile sans lui. La contrepartie est que les correctifs et les sauvegardes sont désormais votre responsabilité, ce qui explique pourquoi Fail2ban et le rituel de restauration ne sont pas optionnels ici.