Date : 2026-03-25
Auteur : Daniel Caron
Version OS : Debian 12 (Bookworm)
Rôle : Gestionnaire de mots de passe — Vaultwarden + PostgreSQL (Docker)
VLAN : 60 (Serveurs)
IP : 10.21.60.17
FQDN : whvd2002.home.carontech.net
URL Vaultwarden : https://vault.carontech.net
whvd2002Note : Le template Debian 12 utilise la clé SSH de Proxmox (pas la clé vmadmin).
Lors du premier accès, se connecter depuis le shell Proxmox :
ssh debianadmin@<IP>— la clé Proxmox est utilisée automatiquement.
Ensuite ajouter la clé vmadmin dans~/.ssh/authorized_keys.
Au premier démarrage, cloud-init effectue automatiquement :
Le boot est plus long qu'habituellement (~2-3 minutes). Appuyer sur Entrée dans la console Proxmox pour afficher le prompt de login.
10.21.60.17| Type | Nom | Valeur |
|---|---|---|
| A | whvd2002.home.carontech.net |
10.21.60.17 |
| CNAME | vault.carontech.net |
whvu1010.home.carontech.net |
Important : Le CNAME
vault.carontech.netdoit pointer verswhvu1010(pas verswhvd2002directement) pour que le trafic passe par le proxy Nginx et le certificat Let's Encrypt.
Depuis le shell Proxmox, se connecter à la VM :
ssh [email protected]
Sur le poste admin Windows, récupérer la clé publique :
Get-Content C:\Users\vmadmin\.ssh\id_ed25519.pub
Sur la VM, ajouter la clé :
echo "ssh-ed25519 AAAA.... vmadmin@hostname" >> ~/.ssh/authorized_keys
Tester depuis le poste admin :
ssh [email protected]
sudo hostnamectl set-hostname whvd2002
sudo nano /etc/hosts
Contenu de /etc/hosts :
127.0.0.1 localhost
127.0.1.1 whvd2002.home.carontech.net whvd2002
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
docker --version
sudo docker run hello-world
sudo usermod -aG docker debianadmin
Se déconnecter et reconnecter, puis vérifier :
docker ps
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden
sudo nano /opt/vaultwarden/docker-compose.yml
version: '3.8'
services:
postgresql:
image: postgres:16
container_name: vaultwarden-db
restart: unless-stopped
environment:
POSTGRES_DB: vaultwarden
POSTGRES_USER: vaultwarden
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/postgresql:/var/lib/postgresql/data
networks:
- vaultwarden-net
expose:
- "5432"
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
depends_on:
- postgresql
environment:
DATABASE_URL: postgresql://vaultwarden:${DB_PASSWORD}@postgresql:5432/vaultwarden
ADMIN_TOKEN: ${ADMIN_TOKEN}
DOMAIN: https://vault.carontech.net
SIGNUPS_ALLOWED: false
INVITATIONS_ALLOWED: true
LOG_LEVEL: warn
TZ: America/Toronto
volumes:
- ./data/vaultwarden:/data
ports:
- "8080:80"
networks:
vaultwarden-net:
driver: bridge
Note : Le port 3012 (WebSocket) est déprécié depuis Vaultwarden v1.29.0. Ne pas l'inclure — tout passe par le port 80 (mappé sur 8080). La variable
WEBSOCKET_ENABLEDest également obsolète.
sudo nano /opt/vaultwarden/.env
sudo chmod 600 /opt/vaultwarden/.env
# Mot de passe PostgreSQL (générer avec : openssl rand -base64 32)
DB_PASSWORD=<mot_de_passe_fort>
# Token admin Argon2 PHC — voir section 6.4 pour la génération
# Guillemets simples obligatoires (caractères spéciaux $)
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
Le token admin doit être hashé en Argon2 PHC. Un token en plain text génère un avertissement dans les logs à chaque démarrage.
# Démarrer temporairement avec un token plain text pour accéder au hash
# Puis générer le hash :
sudo docker exec -it vaultwarden /vaultwarden hash --preset owasp
# → Entrer le mot de passe admin souhaité
# → Copier le résultat $argon2id$... dans .env entre guillemets simples
cd /opt/vaultwarden
sudo docker compose up -d
sudo docker ps --format "table {{.Names}}\t{{.Ports}}"
sudo docker logs vaultwarden 2>&1 | grep -i version
Les deux containers doivent apparaître avec le statut Up :
vaultwardenvaultwarden-dbVersion installée : Vaultwarden 1.35.4
# Activer temporairement les inscriptions
# Dans .env : SIGNUPS_ALLOWED=true
sudo docker compose restart vaultwarden
Accéder à https://vault.carontech.net et créer le compte admin.
# Désactiver immédiatement les inscriptions après création du compte
# Dans .env : SIGNUPS_ALLOWED=false
sudo docker compose restart vaultwarden
Le panel admin est accessible à https://vault.carontech.net/admin.
Note : L'accès au panel admin doit être restreint au LAN uniquement via la configuration Nginx sur whvu1515 (voir section 8.2).
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /home/ubuntuadmin/.secrets/cloudflare.ini \
-d vault.carontech.net \
--agree-tos --no-eff-email --email [email protected]
Fichiers générés :
/etc/letsencrypt/live/vault.carontech.net/fullchain.pem/etc/letsencrypt/live/vault.carontech.net/privkey.pem#---------------------------
# Vaultwarden - proxy depuis whvu1010
#---------------------------
server {
listen 443 ssl;
server_name vault.carontech.net;
ssl_certificate /etc/pki/certs/whvu1515-fullchain.crt;
ssl_certificate_key /etc/pki/private/whvu1515.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
client_max_body_size 525M;
location / {
proxy_pass http://10.21.60.17: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";
}
# WebSocket notifications (port 3012 déprécié — tout passe par 8080)
location /notifications/hub {
proxy_pass http://10.21.60.17:8080/notifications/hub;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
Note : Le SAN
vault.carontech.netdoit être présent dans le certificat PKI de whvu1515. Regénérer le certificat viasign-cert.ps1si nécessaire.
Activation du site :
sudo ln -s /etc/nginx/sites-available/vaultwarden.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
# Force HTTP → HTTPS
server {
listen 80;
server_name vault.carontech.net;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
# Serve Vaultwarden via HTTPS
server {
listen 443 ssl;
server_name vault.carontech.net;
client_max_body_size 525M;
ssl_certificate /etc/letsencrypt/live/vault.carontech.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.carontech.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass https://whvu1515.home.carontech.net:443;
proxy_set_header Host vault.carontech.net;
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";
}
}
Activation :
sudo ln -s /etc/nginx/sites-available/vault.carontech.net /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Le tunnel b710043f-c5c9-48ad-8678-a99197c1f6e2 sur whvu1010 est configuré via le dashboard Cloudflare Zero Trust.
| Service | Public Hostname | Type | URL |
|---|---|---|---|
| Immich | photos.carontech.net |
HTTPS | https://photos.carontech.net |
| Wiki.JS | wiki.carontech.net |
HTTPS | https://wiki.carontech.net |
| Vaultwarden | vault.carontech.net |
HTTPS | https://vault.carontech.net |
Le fichier local est requis pour fournir noTLSVerify: true, option non disponible dans la nouvelle interface du dashboard Cloudflare.
tunnel: b710043f-c5c9-48ad-8678-a99197c1f6e2
credentials-file: /etc/cloudflared/b710043f-c5c9-48ad-8678-a99197c1f6e2.json
ingress:
- hostname: photos.carontech.net
service: https://photos.carontech.net
originRequest:
noTLSVerify: true
- hostname: vault.carontech.net
service: https://vault.carontech.net
originRequest:
noTLSVerify: true
- service: http_status:404
Important : Redémarrer
cloudflaredaprès tout ajout ou modification d'un service dans ce fichier :sudo systemctl restart cloudflared
Access → Applications → Add application → Self-hosted
Name : Vaultwarden
Subdomain : vault
Domain : carontech.net
→ Add policy
Policy name : Admins CaronTech
Action : Allow
Rules :
Selector : Emails
Value : [email protected]
Les apps mobiles et extensions navigateur ne passent pas par le flow browser de Cloudflare Access. Bypasser les paths API :
→ Add policy
Policy name : Bitwarden API Bypass
Action : Bypass
Rules :
Selector : Everyone
Path-based rules :
/api/*
/identity/*
/icons/*
/notifications/*
/attachments/*
| Source | Destination | Port | Action |
|---|---|---|---|
VLAN 60 (whvu1515 — 10.21.60.14) |
VLAN 60 (whvd2002 — 10.21.60.17) |
8080 | Allow |
Note : Le port 8080 est le seul port à exposer. Le port 3012 est déprécié et PostgreSQL (5432) reste interne au réseau Docker.
sudo nano /opt/vaultwarden/backup.sh
sudo chmod +x /opt/vaultwarden/backup.sh
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/opt/backups/vaultwarden
mkdir -p $BACKUP_DIR
# Backup PostgreSQL
docker exec vaultwarden-db pg_dump \
-U vaultwarden vaultwarden \
| gzip > $BACKUP_DIR/db_$DATE.sql.gz
# Backup données Vaultwarden (attachments, config)
tar -czf $BACKUP_DIR/data_$DATE.tar.gz \
/opt/vaultwarden/data/vaultwarden
# Conserver seulement les 30 derniers backups
ls -t $BACKUP_DIR/db_*.sql.gz | tail -n +31 | xargs rm -f
ls -t $BACKUP_DIR/data_*.tar.gz | tail -n +31 | xargs rm -f
echo "Backup complété : $DATE"
sudo crontab -e
# Backup Vaultwarden — quotidien à 2h00
0 2 * * * /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1
| Paramètre | Valeur |
|---|---|
| Fréquence | Quotidienne — 2h00 AM |
| Rétention | 30 jours |
| Destination | /opt/backups/vaultwarden/ |
| Log | /var/log/vaultwarden-backup.log |
# 1. Arrêter les services
cd /opt/vaultwarden && sudo docker compose down
# 2. Restaurer les données Vaultwarden
tar -xzf /opt/backups/vaultwarden/data_YYYYMMDD_HHMMSS.tar.gz -C /
# 3. Restaurer PostgreSQL
sudo docker compose up -d postgresql
gunzip -c /opt/backups/vaultwarden/db_YYYYMMDD_HHMMSS.sql.gz \
| docker exec -i vaultwarden-db psql -U vaultwarden vaultwarden
# 4. Redémarrer Vaultwarden
sudo docker compose up -d vaultwarden
/opt/vaultwarden/
├── docker-compose.yml # Orchestration des containers
├── .env # Secrets (chmod 600)
├── backup.sh # Script de sauvegarde
└── data/
├── postgresql/ # Données PostgreSQL (volume Docker)
└── vaultwarden/ # Données Vaultwarden (attachments, config)
/opt/backups/vaultwarden/ # Backups quotidiens (30 jours de rétention)
Accès externe :
Cloudflare Tunnel → whvu1010:443 (Let's Encrypt) → whvu1515:443 (PKI interne) → whvd2002:8080 (HTTP)
Accès interne :
Station → whvu1010:443 (Let's Encrypt) → whvu1515:443 (PKI interne) → whvd2002:8080 (HTTP)
Internet
|
Cloudflare (vault.carontech.net) — Zero Trust Access Policy
|
whvu1010 — DMZ — Nginx + Certbot (Let's Encrypt) — HTTPS:443
|
whvu1515 — VLAN Serveurs — Nginx proxy interne — HTTPS:443 (cert PKI interne)
|
whvd2002 — VLAN 60 — Vaultwarden Docker — HTTP:8080
|
whvd2002 — VLAN 60 — PostgreSQL Docker — 5432 (interne uniquement)
cd /opt/vaultwarden
# Démarrer
sudo docker compose up -d
# Arrêter
sudo docker compose down
# Redémarrer
sudo docker compose down && sudo docker compose up -d
# Vérifier l'état
sudo docker ps --format "table {{.Names}}\t{{.Ports}}"
# Logs en temps réel
sudo docker compose logs -f vaultwarden
cd /opt/vaultwarden
sudo docker compose pull
sudo docker compose down && sudo docker compose up -d
sudo docker logs vaultwarden 2>&1 | grep -i version
sudo docker exec -it vaultwarden /vaultwarden hash --preset owasp
# Copier le résultat $argon2id$... dans .env entre guillemets simples
sudo docker compose down && sudo docker compose up -d
Symptôme : curl http://10.21.60.17:3012/ → Connection refused
Cause : Le port 3012 (WebSocket séparé) est déprécié depuis Vaultwarden v1.29.0. Le container le déclare dans Docker mais Vaultwarden n'y bind plus rien.
Résolution : Retirer 3012:3012 du docker-compose. Rediriger /notifications/hub vers :8080.
Symptôme : 502 depuis https://vault.carontech.net
Cause : proxy_pass http://10.21.60.17:80 dans Nginx sur whvu1515 (port 80 au lieu de 8080).
Résolution : Corriger pour proxy_pass http://10.21.60.17:8080.
Symptôme : 502 persistant après correction Nginx
Cause : La règle Unifi VLAN 60 autorisait le port 80 seulement.
Résolution : Modifier la règle Unifi pour autoriser le port 8080.
Symptôme : x509: certificate signed by unknown authority dans les logs cloudflared
Cause : cloudflared contactait https://vault.carontech.net sans noTLSVerify: true, contrairement aux autres services déjà configurés dans /etc/cloudflared/daniel.
Résolution : Redémarrer cloudflared (sudo systemctl restart cloudflared) après ajout du Public Hostname dans le dashboard Cloudflare Zero Trust.
Symptôme : [NOTICE] You are using a plain text ADMIN_TOKEN which is insecure dans les logs
Cause : Token admin en clair dans .env.
Résolution : Générer un hash Argon2 PHC via docker exec -it vaultwarden /vaultwarden hash --preset owasp et mettre à jour .env.