# Vaultwarden

<p class="callout info">**En bref**  
**Niveau** : intermédiaire  
**Durée** : 30 minutes  
**Prérequis** : Traefik en place  
**Niveau de test** : reconstitué</p>

## 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.

<table id="bkmrk-%C3%89l%C3%A9ment-r%C3%B4le-contene"><thead><tr><th>Élément</th><th>Rôle</th></tr></thead><tbody><tr><td>Conteneur Vaultwarden</td><td>Serveur, interface web et API compatible Bitwarden</td></tr><tr><td>Dossier `data`</td><td>Base SQLite, pièces jointes, clés et journal</td></tr><tr><td>Journal `vaultwarden.log`</td><td>Lu par Fail2ban pour bloquer les échecs de connexion</td></tr></tbody></table>

**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.

<p class="callout info">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.</p>

## 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

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

```

## Variables

```yaml
# 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

```text
# 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

```

<p class="callout info">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.</p>

## Déploiement

```yaml
# 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.

```bash
# 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

```bash
# 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**.