Skip to main content

Vaultwarden

En bref
Niveau : intermédiaire
Durée : 30 minutes
Prérequis : Traefik en place
Niveau de test : reconstitué

Vaultwarden, c'est quoi ?

Vaultwarden est un gestionnaire de mots de passe auto-hébergé, compatible avec les applications et extensions Bitwarden. Chaque utilisateur y range ses identifiants dans un coffre chiffré, accessible depuis le navigateur, l'ordinateur et le téléphone. Un seul conteneur suffit : le serveur et sa base de données SQLite.

Élément Rôle
Conteneur Vaultwarden Serveur, interface web et API compatible Bitwarden
Dossier data Base SQLite, pièces jointes, clés et journal
Journal vaultwarden.log Lu par Fail2ban pour bloquer les échecs de connexion

Navigateur ou application → Traefik → Vaultwarden

Concrètement, Vaultwarden est l'application la plus simple du catalogue à déployer, et la seule qui ne passe pas par le SSO.

Le coffre est chiffré avec le mot de passe principal de l'utilisateur, qui ne quitte jamais son appareil. Ce mot de passe ne peut pas être délégué à Authentik : le serveur ne pourrait plus rien déchiffrer sans lui. Le rôle ne crée donc pas de connexion SSO pour Vaultwarden. Certaines versions proposent une connexion SSO optionnelle, non activée ici.

Prérequis

  1. Traefik publie vault.penhoat.example (variable client_subdomains.vaultwarden).
  2. vaultwarden figure dans nebuleuse_apps et dans nebuleuse_catalogue.
  3. Le secret vault_vaultwarden_admin_token existe dans le Vault du client (page « Inventaire et variables »).

Arborescence du rôle

roles/vaultwarden/
├── defaults/main.yml
├── tasks/main.yml
└── templates/
    └── docker-compose.yml.j2

Variables

# roles/vaultwarden/defaults/main.yml

# Version de Vaultwarden : remplacer par la dernière version stable.
vaultwarden_version: "1.34.3"

# Dossier de travail sur la VM.
vaultwarden_dir: /opt/nebuleuse/vaultwarden

# Nom de domaine de l'application.
vaultwarden_host: "{{ client_subdomains.vaultwarden }}.{{ client_domain }}"

# Inscriptions : fermées en fonctionnement normal. À ouvrir temporairement pour créer
# les premiers comptes (voir la section « Créer les premiers comptes »).
vaultwarden_signups_allowed: false

# Seules les adresses de ce domaine peuvent s'inscrire quand les inscriptions sont ouvertes.
vaultwarden_signups_domain: "{{ client_domain }}"

# Panneau d'administration (/admin) : désactivé par défaut. Il donne accès à tous les comptes.
vaultwarden_admin_enabled: false

# Jeton du panneau d'administration, stocké dans le Vault sous forme de hachage Argon2.
vaultwarden_admin_token: "{{ vault_vaultwarden_admin_token }}"

# Réseau Docker partagé avec Traefik.
vaultwarden_network: "{{ docker_proxy_network }}"

Le fichier Docker Compose

# roles/vaultwarden/templates/docker-compose.yml.j2
# Fichier géré par Ansible : toute modification manuelle sera écrasée.
# Il peut contenir un jeton d'administration : accès réservé à root.

services:
  vaultwarden:
    image: vaultwarden/server:{{ vaultwarden_version }}
    container_name: {{ client_name }}_vaultwarden
    restart: unless-stopped
    environment:
      # Adresse publique : utilisée dans les liens des e-mails et par les extensions.
      DOMAIN: "https://{{ vaultwarden_host }}"
      SIGNUPS_ALLOWED: "{{ vaultwarden_signups_allowed | lower }}"
      SIGNUPS_DOMAINS_WHITELIST: "{{ vaultwarden_signups_domain }}"
      # Les comptes existants peuvent inviter d'autres personnes (organisations).
      INVITATIONS_ALLOWED: "true"
      # Journal dans un fichier : c'est lui que lit Fail2ban.
      LOG_FILE: /data/vaultwarden.log
      LOG_LEVEL: warn
      TZ: Europe/Paris
{% if vaultwarden_admin_enabled %}
      # Un hachage Argon2 contient des "$", qui doivent être doublés dans un fichier Compose.
      ADMIN_TOKEN: '{{ vaultwarden_admin_token | replace("$", "$$") }}'
{% endif %}
    volumes:
      - {{ vaultwarden_dir }}/data:/data
    networks:
      - {{ vaultwarden_network }}
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network={{ vaultwarden_network }}"
      - "traefik.http.routers.vaultwarden-{{ client_name }}.rule=Host(`{{ vaultwarden_host }}`)"
      - "traefik.http.routers.vaultwarden-{{ client_name }}.entrypoints=websecure"
      # Port d'écoute de Vaultwarden dans le conteneur.
      - "traefik.http.services.vaultwarden-{{ client_name }}.loadbalancer.server.port=80"

networks:
  {{ vaultwarden_network }}:
    external: true

Vaultwarden lit l'adresse du visiteur dans l'en-tête X-Real-IP, que Traefik renseigne. Les échecs de connexion de son journal portent donc la vraie adresse du visiteur, ce qui permet à Fail2ban de la bloquer.

Déploiement

# roles/vaultwarden/tasks/main.yml

- name: Créer les dossiers de Vaultwarden
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    mode: "0755"
  loop:
    - "{{ vaultwarden_dir }}"
    - "{{ vaultwarden_dir }}/data"

- name: Déposer le fichier Docker Compose de Vaultwarden
  ansible.builtin.template:
    src: docker-compose.yml.j2
    dest: "{{ vaultwarden_dir }}/docker-compose.yml"
    # Le fichier peut contenir un jeton : root uniquement.
    mode: "0600"
  no_log: true

- name: Démarrer Vaultwarden
  community.docker.docker_compose_v2:
    project_src: "{{ vaultwarden_dir }}"
    state: present

- name: Attendre que Vaultwarden réponde
  ansible.builtin.command:
    argv:
      - curl
      - -fsS
      - -k
      - --resolve
      - "{{ vaultwarden_host }}:443:127.0.0.1"
      - "https://{{ vaultwarden_host }}/alive"
  register: vaultwarden_alive
  until: vaultwarden_alive.rc == 0
  retries: 30
  delay: 5
  changed_when: false

Créer les premiers comptes

Les inscriptions sont fermées par défaut : personne ne peut créer de compte sur l'adresse publique. Pour intégrer un client, le support ouvre les inscriptions le temps de créer les premiers comptes, puis les referme.

# 1. Ouvrir les inscriptions pour ce client, pour les adresses de son domaine seulement.
ansible-playbook site.yml --limit client-alpha --tags vaultwarden -e vaultwarden_signups_allowed=true

# 2. Le client crée son compte sur https://vault.penhoat.example (mot de passe principal choisi par lui).

# 3. Refermer les inscriptions : relancer sans la variable.
ansible-playbook site.yml --limit client-alpha --tags vaultwarden

Les comptes suivants sont créés par invitation depuis une organisation, sans rouvrir les inscriptions.

Vérification

# Sur la VM : le conteneur est démarré.
docker ps --filter name=vaultwarden --format '{{.Names}}\t{{.Status}}'

# L'adresse publique répond : doit afficher une date ISO.
curl -sS https://vault.penhoat.example/alive

# Le fichier de journal existe (créé au premier événement).
ls -l /opt/nebuleuse/vaultwarden/data/vaultwarden.log

Dans le navigateur, https://vault.penhoat.example affiche la page de connexion. Si les inscriptions sont fermées, le bouton « Créer un compte » est absent.

Pièges

  1. Inscriptions laissées ouvertes : une instance publique avec les inscriptions ouvertes accepte n'importe quelle adresse du domaine autorisé, sans vérification. La variable vaultwarden_signups_allowed doit revenir à false dès les comptes créés.
  2. Jeton d'administration en clair : le jeton doit être stocké sous forme de hachage Argon2 dans le Vault, pas en clair. Dans le fichier Compose, chaque $ doit être doublé, ce que fait le modèle avec replace.
  3. Panneau d'administration exposé : il donne accès à tous les comptes. Il reste désactivé tant que vaultwarden_admin_enabled n'est pas passé à true, et ne doit l'être que le temps d'une intervention.
  4. Mot de passe principal perdu : le serveur ne peut ni le retrouver ni le réinitialiser, car il ne le connaît pas. Sans lui, le coffre est perdu. Prévenir le client à l'ouverture de son compte.
  5. Extensions qui n'atteignent pas le serveur : l'adresse du serveur doit être saisie en entier, https://vault.penhoat.example, et le certificat doit être reconnu. Les certificats de test de Let's Encrypt sont refusés par les applications.
  6. Journal absent pour Fail2ban : sans LOG_FILE, Vaultwarden écrit sur la sortie standard et le filtre ne trouve rien. Le chemin hôte du journal est /opt/nebuleuse/vaultwarden/data/vaultwarden.log.
  7. Sauvegarde : tout le contenu est dans le dossier data. Une copie du dossier à froid, ou une copie de la base avec sqlite3 ... ".backup", suffit. Une copie à chaud du seul fichier de base peut être corrompue.

Page suivante : Fail2ban.