Procédure Break-Glass
Document de reconstruction du homelab en cas d'incident majeur (incendie, foudre, defaillance materielle, corruption).
Cette documentation est hébergée sur GitHub Pages, hors du lab. Une panne peut néanmoins vous priver du réseau ou des moyens d’accès : garder une copie papier ou PDF à jour avec YK2.
Objectifs de reprise
| Metrique | Cible | Justification |
|---|---|---|
| RTO (Recovery Time Objective) | 4h | Zero a services critiques operationnels |
| RPO (Recovery Point Objective) | 24h | Backup quotidien a 3h du matin |
| Services critiques | DNS, Vaultwarden, Traefik, Authelia | Tout le reste en dépend |
Architecture de référence
Connaitre l'architecture est indispensable pour savoir quoi restaurer et dans quel ordre.
Dépendances critiques (ordre de démarrage)
1. DNS → AdGuard Home (penny ou dns-failover)
2. Reverse Proxy → Traefik (penny) — necessite CF_DNS_API_TOKEN
3. Passwords → Vaultwarden (LXC 102 galahad) — PAS de dependance SSO
4. SSO → Authelia (penny) — necessite secrets generes
5. Tout le reste → depend de 1-4
Il n'utilisé PAS Authelia pour éviter une dépendance circulaire. Il reste accessible même si Authelia est en panne.
Preparation AVANT l'incident
YubiKeys break-glass (remplacé la clé USB depuis 2026-04-19)
Deux YubiKeys sont des recipiants sops indépendants. Chacune peut déchiffrer TOUS les secrets sans l'autre.
| YubiKey | Localisation | Usage |
|---|---|---|
| YK1 | Domicile (acces quotidien si besoin) | DR sur host fresh + rotation secrets |
| YK2 | Hors-domicile (coffre ou lieu sur) | Redondance pure — stocker avec ce doc imprime |
Secrets couverts par les YubiKeys (dans homelab-config/) :
.env.enc— tous les tokens (Cloudflare, Tailscale, R2, ntfy...)authelia/secrets/*— JWT, OIDC, session, storage keyscrowdsec/online_api_credentials.yaml,local_api_credentials.yamlsystem/secrets/restic-env.enc— password restic + credentials Cloudflare R2 (AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)system/secrets/postfix-relay.env.enc— credentials Proton SMTP submission (token)
Workflow de déchiffrement avec YubiKey (sur n'importe quel host avec age-plugin-yubikey installe) :
# Installer age-plugin-yubikey (macOS)
brew install age-plugin-yubikey
# Exporter l'identite YubiKey (PIN + touch requis)
age-plugin-yubikey --identity > /tmp/yk-id.txt
# Dechiffrer un fichier sops
SOPS_AGE_KEY_FILE=/tmp/yk-id.txt sops --input-type dotenv --output-type dotenv -d system/secrets/restic-env.enc
SOPS_AGE_KEY_FILE=/tmp/yk-id.txt sops -d authelia/secrets/jwt_secret
# Supprimer l'identity apres usage
rm /tmp/yk-id.txt
Coffre physique (hors-domicile, avec YK2)
- YubiKey 2 (YK2) physique
- PIN YK2 (note séparée ou Vaultwarden)
- Master password Vaultwarden (memorise ou papier scelle)
- Codes de récupération TOTP
- Ce document en PDF
- Clé USB portant
homelab-break-glass-<date>.tar.age— voir l'encadré
Le drill du matin a buté sur ceci : homelab-config est privé, un git clone anonyme échoue, et c'est la toute première commande de la phase 3.
$ git ls-remote https://github.com/GabinSMD/homelab-config.git
fatal: could not read Username for 'https://github.com'
La chaîne se refermait sur elle-même : pour cloner il faut un identifiant
GitHub ; cet identifiant est dans .env.enc, qui est dans le dépôt ; ou
dans Vaultwarden, qui tourne sur le cluster qu'on reconstruit justement.
C'est l'option 2 qui a été retenue — une copie hors-ligne chiffrée — parce qu'elle ne dépend d'aucun tiers pendant la reprise : ni GitHub, ni Cloudflare, ni Vaultwarden. Un PAT dans le coffre aurait laissé la reprise suspendue à la disponibilité (et à la non-expiration) d'un service extérieur.
L'archive est produite par scripts/break-glass-bundle.sh
dans homelab-config :
./break-glass-bundle.sh # sortie dans /mnt/ssd/break-glass
./break-glass-bundle.sh /media/cle-usb # directement sur la clé du coffre
Elle contient un git bundle --all des deux dépôts (donc toute l'histoire,
.env.enc et system/secrets/ compris), un répertoire hors-git/ avec les deux
fichiers Authelia que .gitignore exclut (depuis le 2026-09-23, voir plus bas), et un
manifeste avec les SHA de HEAD et les commandes de reprise. Chiffrée avec age vers les
trois mêmes destinataires que sops : clé logicielle de penny, YK1, YK2 — chacun ouvre
seul, donc YK2, qui est déjà dans ce coffre, suffit.
Taille réelle : 4,6 Mo au 2026-09-24 (3,9 Mo au drill du 26/08), pas les 285 Mo estimés pendant le drill — un git bundle ne transporte que l'historique versionné, pas les 264 Mo de données
d'exécution non suivies qui traînaient alors dans l'arborescence (sucre/ à l'époque, base
SQLite d'Authelia, journaux). Le script vérifie son propre produit avant de
rendre la main : déchiffrement réel, clonage des deux bundles, comparaison des
HEAD, et présence de .env.enc dans le clone.
Ce que l'archive ne contient pas : l'état d'exécution non versionné — la base Authelia (donc les inscriptions 2FA), les bases CrowdSec, les volumes Docker. Ça, c'est le périmètre de restic/R2 et de PBS. L'archive débloque la phase 3, elle ne remplace pas les sauvegardes.
À refaire après toute rotation de secret ou changement de configuration notable. Le recopiage sur la clé du coffre n'est plus un geste à retenir : il est automatisé depuis le 2026-08-30, voir la section suivante.
Dépôt sur la clé du coffre — automatisé depuis le 2026-08-30
Le geste « recopier l'archive sur la clé » est resté en attente plusieurs jours
après le drill du 26/08. Un geste manuel qu'on doit se rappeler de faire n'est
pas une procédure, c'est une intention. Désormais : brancher la clé COFFRE
sur penny suffit.
Ce qui se déroule alors, sans intervention :
- udev reconnaît le volume par le label du système de fichiers (
COFFRE), jamais par une lettre de disque — le SSD de penny est lui-même en USB et se ré-énumère (sda1→sdb1pendant l'incident du 29/08). Une règle qui viserait « le disque USB » pourrait viser le SSD. break-glass-auto-depot.shrégénère l'archive si des commits sont plus récents qu'elle, la dépose, puis démonte et remonte la clé avant de relire le sha256. Une relecture immédiate viendrait du cache de pages et validerait une clé défaillante, pleine ou arrachée trop tôt.- La clé est démontée en fin de course : elle se débranche sans précaution.
- Confirmation par ntfy, échec compris.
Le rappel est assuré par break-glass-fraicheur.sh (timer quotidien), qui lit la
trace laissée dans /var/lib/break-glass/dernier-depot :
- urgent si un secret a bougé depuis l'archive (
.env.enc,.sops.yaml,authelia/secrets,system/secrets) — la copie du coffre n'ouvre plus la production ; - routine passé 60 jours.
Il ne compte volontairement pas les commits : une journée de travail sur la documentation en produit dix, dont aucun ne change quoi que ce soit à la reprise. Un rappel qui se déclenche tous les jours n'informe plus.
Elle n'est branchée que le temps du dépôt. Laissée en permanence sur penny, elle partagerait son sort — rançongiciel, effacement, incendie, vol — et cesserait d'être un break-glass. C'est précisément ce dont elle protège.
Depuis un Mac
break-glass-depot-macos.sh fait la même chose quand la clé est branchée sur le
MacBook. Trois pièges, tous rencontrés à la première exécution réelle — le
script avait été écrit et testé depuis penny, en root, donc le vrai chemin
n'avait jamais été parcouru :
PermitRootLogin nofait arriver la session SSH en utilisateur normal. Il faut donc que l'archive soit lisible par son groupe (corrigé dansbreak-glass-bundle.sh), que/var/lib/break-glasslui soit accessible en écriture, et que git accepte un dépôt appartenant à root (safe.directory).- Le script fait six aller-retours SSH : avec une clé matérielle, six saisies de
PIN. Ajouter
ControlMaster autodans le~/.ssh/configdu Mac. - Si le démontage échoue (fenêtre Finder ouverte sur le volume), le script s'arrête plutôt que de valider une copie non vérifiée. Fermer la fenêtre et relancer, ne pas contourner.
Leçon générale : un script d'urgence testé depuis la machine qui l'héberge n'est pas testé.
Comptes externes (doivent rester actifs)
| Compte | Usage | Vérification |
|---|---|---|
| Cloudflare R2 EU | Bucket homelab-backups (s3 endpoint <account-id>.eu.r2.cloudflarestorage.com) | set -a; . /root/.restic-env; set +a; restic snapshots |
| Proton Mail | [email protected] (custom domain) + SMTP submission token | login proton.me, voir /account/u/0/mail/imap-smtp |
| Cloudflare | Zone gabin-simond.fr, token DNS:Edit | Dashboard Cloudflare |
| Tailscale | Tailnet, 3 machines | login.tailscale.com |
| GitHub | Repos homelab-config + homelab-doc | github.com/GabinSMD |
| ntfy.sh | Topic randomise (voir .env ou scripts) | Notification test |
Scénarios de panne
Scénario 0 : Panne d'un seul LXC (dns-failover / vault / logs)
Durée estimee : 15-30 min | Impact : service concerne uniquement
- LXC 100 — dns-failover (AdGuard secondaire)
- LXC 102 — vault (Vaultwarden)
- LXC 101 — logs (Loki + Grafana)
Impact : DNS secondaire indisponible. Le primaire (penny) prend tout le trafic. Aucune interruption visible.
# Depuis galahad
ssh gabins@pve1
# Verifier l'etat
pct status 100
# Tenter un restart
pct start 100
# Si le LXC est corrompu : recreer depuis le template
pct destroy 100
pct create 100 local:vztmpl/debian-12-standard_*.tar.zst \
--hostname dns-failover --memory 256 --cores 1 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.30/24,gw=192.168.1.254 \
--unprivileged 1 --start 1
# Installer AdGuard Home + Tailscale + restaurer config depuis git
pct exec 100 -- bash -c "curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v"
# Copier la config AdGuard depuis homelab-config
S'assurer que l'app mobile Bitwarden est synchro avec les données recentes AVANT de detruire le LXC.
ssh gabins@pve1
pct status 102
pct start 102
# Si corrompu : verifier les backups d'abord
set -a; . /root/.restic-env; set +a # `source` seul ne transmet RIEN a restic
restic snapshots # verifier les backups disponibles
# Recreer le LXC
pct destroy 102
pct create 102 local:vztmpl/debian-12-standard_*.tar.zst \
--hostname vault --memory 256 --cores 1 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.32/24,gw=192.168.1.254 \
--unprivileged 1 --start 1
# Installer Docker + Vaultwarden, restaurer le volume
Impact : pas de logs centralises ni dashboards. Aucun impact sur les autres services.
ssh gabins@pve2
pct status 101
pct start 101
# Si besoin : recreer et reconfigurer Grafana OIDC depuis Authelia
Scénario 1 : penny mort, cluster Proxmox OK
Durée estimee : 1-2h | Impact : DNS primaire, Traefik, Authelia, Homepage — mais Vaultwarden reste UP
Vaultwarden (LXC 102) et AdGuard secondaire (LXC 100) restent operationnels. Les mots de passe et la resolution DNS de base continuent de fonctionner.
Etabli par mesure le 2026-09-06. Le playbook ne remplace pas encore ce parcours : sa couche 1 s'arrete sur penny (voir plus bas). Suivre de haut en bas.
- Phase 1 — acquerir le materiel, flasher DietPi. Manuel pour toujours.
- Phase 2 — premier boot, mot de
passe, clef SSH, monter le SSD, poser
git,age,sops,ansible-core. Manuel pour toujours. - Phase 3 — ouvrir le coffre, sortir la YubiKey, cloner les depots, desceller les VRAIS secrets. Manuel pour toujours. Garder cet ordre : la couche 1 ne pose plus de fixture hors du banc depuis le 2026-09-06 (voir le point 3 des pieges), mais l'ordre reste celui qui a du sens.
- Repli manuel, phase 3bis
— les configs systeme. Lancer d'abord la couche 3 en lecture seule
(
bootstrap-pi.yml --check --diff) : elle dit ce que le depot ferait perdre. Puis copier. - Repli manuel, phases 4 a 6 — Docker, volumes depuis R2, Authelia, reseau, scripts.
verify.yml— le juge. Celui-la tourne sur penny :rc=0, mesure le 2026-09-06.
Ce que le playbook prend en charge sur penny aujourd'hui : l'etape 6, et l'etape 4 en lecture. La couche 1 est bloquee ; la couche 2 n'est bloquee par aucune garde mais n'a jamais ete jouee pour de vrai contre penny — le repli manuel reste donc la reference.
Phase 1 — Materiel (T+0 a T+15min)
- Acquerir le materiel de remplacement :
- Raspberry Pi 4 (8 Go RAM) + alimentation officielle 5.1V/3A
- Carte SD 64 Go (ou récupérer l'existante si intacte)
- SSD USB (ou récupérer l'existant)
- Flasher DietPi sur la SD depuis un autre appareil (dietpi.com)
Phase 2 — Amorce (T+15min a T+30min)
Ce qui suit est hors de portee de tout playbook : il faut d'abord une machine joignable, un disque monte, et l'outil qui jouera la reconstruction.
# Premier boot : connexion console locale, mdp default DietPi
# Changer le password root immediatement
passwd
# Installer la cle SSH
mkdir -p ~/.ssh && chmod 700 ~/.ssh
# Copier la cle publique depuis la cle USB ou depuis GitHub
curl -s https://github.com/GabinSMD.keys >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# Configurer le SSD (si nouveau, formater en ext4 avec le meme UUID)
# Si SSD recupere : monter directement
mkdir -p /mnt/ssd
mount /dev/sda1 /mnt/ssd
# Les outils dont la PHASE 3 a besoin, avant tout playbook.
# La couche 1 les pose aussi — trop tard : elle ne tourne qu'apres le coffre.
apt install -y git age
# sops par binaire verifie par somme, jamais curl-pipe-shell.
# Recette reprise mot pour mot de bootstrap-base.yml (couche 1).
cd /tmp
VER=$(curl -fsSL https://api.github.com/repos/getsops/sops/releases/latest \
| grep -oP '"tag_name": *"v\K[^"]+')
ARCH=$(dpkg --print-architecture)
curl -fsSLO "https://github.com/getsops/sops/releases/download/v${VER}/sops-v${VER}.linux.${ARCH}"
curl -fsSLO "https://github.com/getsops/sops/releases/download/v${VER}/sops-v${VER}.checksums.txt"
grep " sops-v${VER}.linux.${ARCH}$" "sops-v${VER}.checksums.txt" | sha256sum -c -
install -m 0755 "sops-v${VER}.linux.${ARCH}" /usr/local/bin/sops
age-plugin-yubikey n'existe pas dans Debianage oui — 1.1.1-1+b3 en bookworm, verifie le 2026-09-06. Le plugin
YubiKey, non : il faut son binaire de release GitHub, ou une machine tierce.
Si la YubiKey est le seul recipiendaire disponible et qu'aucun binaire n'est
joignable, la reprise est bloquee des la phase 3.
Parade : deposer sur la cle du coffre, a cote de l'archive, le binaire
age-plugin-yubikey pour linux/arm64. Il ne coute que quelques megaoctets
et il ferme une dependance a Internet dans le seul scenario ou l'on suppose
justement Internet absent.
ansible-core n'est declare nulle part — a poser a la mainLa reconstruction n'installe pas son propre outil. Mesure sur penny le
2026-09-06 : ansible-playbook y est un lien vers
/root/.local/share/uv/tools/ansible-core/bin/, pose par uv tool install.
Il n'est ni dans paquets_base (la liste de la couche 1), ni dans un
manifeste de deploiement, ni dans aucune sauvegarde de binaire.
Debian bookworm propose ansible-core 2.14.18 ; penny joue ces playbooks
sous 2.21.3. L'ecart de version n'a jamais ete eprouve — si le paquet
Debian est le seul recours en reprise, s'attendre a devoir lire des erreurs de
syntaxe avant que ca tourne.
Phase 3 — Ouvrir le coffre (T+30min a T+45min)
Physique, et irremplacable : c'est ici qu'on sort la YubiKey et qu'on ouvre l'archive. Aucun playbook ne peut le faire a votre place.
# Depuis la cle USB du coffre — aucune authentification GitHub requise.
# YK2 est dans le meme coffre ; -i peut aussi pointer une cle age logicielle.
cd /mnt/ssd
age -d -i <cle-ou-yubikey> /media/cle-usb/homelab-break-glass-<date>.tar.age > bg.tar
tar xf bg.tar # -> homelab-config.bundle, homelab-doc.bundle, MANIFESTE.txt, hors-git/
# LE CHEMIN COMPTE : les playbooks exigent le depot a /mnt/ssd/config
# (`repo_root`), pas dans un repertoire de travail voisin.
#
# ET `git clone` REFUSE un repertoire non vide — « destination path already
# exists and is not an empty directory ». C'est le cas le PLUS PROBABLE ici :
# la phase 2 propose de reutiliser le SSD, qui porte alors deja le depot.
# Les trois branches ci-dessous ont ete eprouvees le 2026-09-06.
if [ -d /mnt/ssd/config/.git ]; then
echo "Depot deja present — NE RIEN ECRASER, c'est peut-etre plus a jour que le coffre."
git -C /mnt/ssd/config log --oneline -1
git -C /mnt/ssd/config status --short # des modifications non commitees ?
grep -A3 homelab-config MANIFESTE.txt # comparer au HEAD de l'archive
elif [ -e /mnt/ssd/config ]; then
# Present mais sans .git : on met de cote, on n'ecrase jamais.
mv /mnt/ssd/config "/mnt/ssd/config.avant-reprise-$(date +%Y%m%d-%H%M%S)"
git clone homelab-config.bundle /mnt/ssd/config
else
git clone homelab-config.bundle /mnt/ssd/config
fi
git clone homelab-doc.bundle /mnt/ssd/doc-repo # ce runbook, hors ligne
# Repli si la cle USB est perdue ET qu'un acces GitHub est disponible :
# git clone https://github.com/GabinSMD/homelab-config.git /mnt/ssd/config
# Poser la cle age la ou la production la cherche, puis desceller POUR DE VRAI.
# C'est la seule etape de secrets du parcours : ce que fait la couche 1 sur ce
# terrain est une FIXTURE de banc (voir l'avertissement plus bas).
mkdir -p /root/.config/sops/age
age-plugin-yubikey --identity > /root/.config/sops/age/keys.txt
chmod 600 /root/.config/sops/age/keys.txt
/mnt/ssd/config/scripts/homelab-unseal.sh # -> /run/homelab/.env (+ authelia, crowdsec)
Ce bloc avertissait du contraire jusqu'au 2026-09-24. Il etait perime — et c'etait le pire endroit pour l'etre, puisqu'on ne lit cette page qu'en situation, ou elle aurait envoye sur un contournement devenu inutile.
Le probleme, tel qu'il etait. authelia/configuration.yml (les 10 clients
OIDC) et authelia/users_database.yml (le compte) sont exclus par
.gitignore lignes 15-16, parce qu'ils portent des empreintes de secrets.
break-glass-bundle.sh ne produisait que des git bundle --all : il
n'embarquait donc que du suivi git. Une Pi reconstruite depuis la seule cle
du coffre n'avait aucune authentification, donc aucun service joignable.
Ce qui a change. Le script copie desormais ces deux fichiers dans un
repertoire hors-git/ de l'archive. Ils ne voyagent pas en clair : l'archive
entiere est scellee par age, exactement comme les bundles et .env.enc.
Apres tar xf bg.tar, ils sont donc la :
ls hors-git/authelia/ # configuration.yml users_database.yml
cp -a hors-git/authelia/*.yml /mnt/ssd/config/authelia/
Trois gardes protegent cette promesse, et toutes jugent le travail accompli plutot que l'intention :
| Garde | Ce qu'elle exige |
|---|---|
| Etape 2b | un fichier declare mais illisible arrete le script |
| Etape 4 | on relit le tar et on exige que chaque fichier annonce y soit — compter les entrees de la liste reviendrait a se comparer a soi-meme, et vider la liste rendrait vert |
| Etape 5 | l'aller-retour age compare les empreintes apres dechiffrement, pas la simple presence : un fichier tronque existe |
Le defaut avait ete trouve le 2026-09-06. Le drill du 26/08 ne l'avait pas vu parce qu'il prouvait la restauration des donnees, pas celle de la pile de configuration.
Phases 4 a 6 — le playbook, et ce qu'il ne fait pas encore sur penny
Ce que faisaient a la main les phases 4 (Docker + services), 5 (Authelia) et 6 (reseau + verification), et la moitie automatisable de la phase 3 (les configs systeme), est desormais un playbook — decrit en detail dans Derive de configuration. Il est eprouve contre un banc jetable — contrairement a ce runbook, qui a pourri trois mois et demi sans que personne le sache.
La couche 1 s'arrete aujourd'hui sur penny, les trois exigent un drapeau explicite, et la couche 3 n'a jamais tourne pour de vrai. Tout est mesure et detaille juste dessous ; un repli manuel complet suit plus bas.
cd /mnt/ssd/config/ansible
# Couche 1 — paquets, Docker, sops, Tailscale (installe, PAS enrole), Alloy
ansible-playbook playbooks/bootstrap-base.yml -e cible=penny \
-e bootstrap_autorise=true -e age_key_path=/root/.config/sops/age/keys.txt
# Couche 2 — socket-proxy, traefik, authelia, adguard, homepage
ansible-playbook playbooks/bootstrap-stack.yml -e cible=penny -e bootstrap_autorise=true
# Couche 3 — Pi et DietPi. aarch64 uniquement. LIRE le --check avant d'appliquer.
ansible-playbook playbooks/bootstrap-pi.yml -e bootstrap_autorise=true --check --diff
ansible-playbook playbooks/bootstrap-pi.yml -e bootstrap_autorise=true
# Le verdict
ansible-playbook playbooks/verify.yml -e cible=penny
verify.yml rend le verdict. Il ne dit pas « les conteneurs sont demarres »
mais « les services SERVENT » — un 302 d'Authelia vient du middleware et non du
backend, et CyberChef est reste mort 24 h derriere un 302. Il porte aussi un
controle negatif : un hote qui ne peut correspondre a aucun routeur doit
obtenir une reponse DIFFERENTE, faute de quoi la sonde mesurerait une
redirection inconditionnelle au lieu d'un routage.
Execute le 2026-09-06 sur les deux cibles :
| Cible | Resultat |
|---|---|
| banc (LXC 111) | ok=11 failed=0 — 14 clefs descellees |
| penny (production) | ok=11 failed=0 — 37 clefs descellees |
Le 2026-09-06, les trois couches ont ete jouees contre penny en lecture seule. Voici ce qu'elles rendent, mot pour mot. Lire avant, pas pendant.
1. bootstrap_autorise est a false pour penny, exprès. Sans le drapeau :
$ ansible-playbook playbooks/bootstrap-base.yml -e cible=penny --check
fatal: [penny]: FAILED! => penny n'autorise pas ce playbook
(bootstrap_autorise = False). Cette couche installe des dizaines de paquets,
ajoute trois depots apt tiers et purge le cache apt de la cible.
C'est un cran de securite, pas un defaut : penny est la production. En reprise apres sinistre, c'est bien l'intention — on passe le drapeau.
2. La couche 1 ne tourne PAS sur penny aujourd'hui. Avec le drapeau, elle s'arrete a sa troisieme tache :
$ ansible-playbook playbooks/bootstrap-base.yml -e cible=penny \
-e bootstrap_autorise=true -e age_key_path=/root/.config/sops/age/keys.txt --check
TASK [Paquets de base] *********************************************************
fatal: [penny]: FAILED! => Error while resolving value for 'name':
'paquets_base' is undefined
paquets_base n'est declare que dans inventory/group_vars/banc.yml. Les
couches 1 et 2 ont ete construites pour le banc ; les porter sur penny
demande de decider quelle liste de paquets penny recoit — une decision, pas
une variable a recopier. Tant qu'elle n'est pas prise, les phases 4 a 6 ne
sont pas automatisees de bout en bout sur penny, et il faut retomber sur les
gestes de l'ancien runbook, conserves plus bas.
3. La couche 1 scelle une FIXTURE — et depuis le 2026-09-06, sur le banc
SEULEMENT. Ses trois dernieres taches chiffrent
ansible/fixtures/banc/.env.plain — des valeurs factices — et les descellent
vers /run/homelab/.env, le fichier meme que lit la production.
Jusqu'au 2026-09-06 elles n'etaient pas conditionnees au banc, et leur seul
garde-fou etait un creates: : sur une penny fraiche, /run/homelab est vide,
donc ce garde-fou ne retenait rien. Les 14 valeurs sont factices mais les 14
noms sont ceux de la production : tous les ${VAR:?} du compose auraient
ete satisfaits, rien n'aurait crie, et la couche 2 — banc_mode faux, donc
ACME arme — serait partie demander un certificat pour le vrai domaine avec
CF_DNS_API_TOKEN=banc-cloudflare-factice. Un defi DNS-01 en rafale, et le
quota Let's Encrypt du domaine brule. Ce qui l'empechait n'etait qu'un accident
de plus : la couche 1 mourait sur paquets_base avant d'y arriver.
Les trois taches portent desormais when: banc_mode | bool, le meme mecanisme
que les vingt-huit taches de surcharge de la couche 2. Verifie en le
provoquant, le 2026-09-06 : -e cible=penny -e bootstrap_autorise=true
rend skipping: [penny] sur les trois, et ok=12 skipped=0 sur le banc.
L'ordre de cette page reste le bon — la phase 3 desselle les VRAIS secrets avant la couche 1 — mais ce n'est plus lui qui vous protege.
4. --check n'est pas un apercu valide de la couche 2. Ses commandes
docker compose sont sautees en mode verification, et l'attente qui suit
rapporte alors « aucun conteneur » sur une machine qui en fait tourner cinq
(mesure sur penny). Un --check rouge sur la couche 2 ne prouve rien.
Ce que le playbook ne fait pas, et qui reste ci-dessous : les 17 autres
services de la stack (docker/docker-compose.yml en declare 22, moins les
cinq de la couche 2 — compte du 2026-09-06), la restauration des volumes depuis R2 (phase 4 du repli
manuel), la configuration Authelia (phase 5), les certificats TLS reels — la couche 2 pose
un certificat auto-signe sur le banc et laisse ACME desarme — et l'enrolement
Tailscale, qui exige une clef d'authentification et reste un geste humain, au
meme titre que l'ouverture du coffre.
Il n'exprime pas non plus l'ordre de demarrage des dependances : c'est le tableau en tete de cette page, et lui seul.
Elle ne peut pas etre eprouvee sur le banc : un LXC amd64 n'a ni
/boot/firmware, ni carte SD, ni DietPi, ni vcgencmd. Ce qui a ete fait a la
place — penny comme oracle, --check --diff, cinq fautes provoquees contre le
pre-vol — prouve la non-regression, pas la reconstruction.
Le pre-vol de non-perte ARRETERA la premiere vraie reconstruction. C'est
certain, et c'est voulu : une image DietPi fraiche porte des directives que
penny n'a pas (dtparam=audio=on la ou penny a off, un quiet ou un
fsck.repair=yes dans cmdline.txt, des lignes de config.txt posees par
defaut). Chacune sortira en PERDUE et l'assertion tombera. La marche a suivre
est dans le message d'echec : verser au depot ce qu'on garde, inscrire dans
pi_abandons ce qu'on laisse tomber. Qui decouvre ca en pleine reconstruction
prendra une garde qui fonctionne pour un playbook casse.
Deux divergences de config.txt ne sont pas tranchees. Mesure le
2026-09-06 par --check --diff sur penny — c'est le seul changed de tout le
playbook :
--- before: /boot/firmware/config.txt
+++ after: /mnt/ssd/config/boot/config.txt
-max_framebuffers=1
+max_framebuffers=0
-#hdmi_ignore_hotplug=1
+hdmi_ignore_hotplug=1
Le pre-vol ne mord pas dessus, et il a raison : max_framebuffers est une cle
scalaire presente des deux cotes (la valeur du depot fait foi), et
hdmi_ignore_hotplug est un ajout, pas une perte. Une execution reelle
remettrait donc les valeurs du depot — moins de memoire video, HDMI ignore.
C'est vraisemblablement l'intention d'origine sur une machine sans ecran, mais
personne ne l'a decide. A trancher avant, pas pendant.
/boot/dietpi.txt est ecrit par la couche 3 et n'est pas versionne. La
fonction dietpi-set_swapfile y persiste AUTO_SETUP_SWAPFILE_SIZE et
AUTO_SETUP_SWAPFILE_LOCATION. Aucune sonde ne le compare a quoi que ce soit.
Reserve connue, pas un oubli.
Vouloir verifier cette phrase a ouvert un defaut bien plus grave, corrige le jour meme et raconte en detail dans Sauvegardes. Le resume, parce qu'il change ce que vaut une restauration a froid :
homelab_backup.sh construisait sa liste de chemins avec
[ -d "$p" ] && BACKUP_PATHS+=("$p"). -d ne repond vrai que pour un
repertoire : toute entree de type FICHIER etait jetee sans un mot, et la
sauvegarde se declarait « OK ». Deux entrees en sont mortes —
/boot/firmware/dietpi.txt depuis le 2026-05-11, et l'etat OpenTofu des dix
LXC depuis le 2026-09-04. Verifie contre le depot R2 : aucun dietpi.txt
dans aucun snapshot, et l'etat absent des nuits du 05 et du 06/09.
Au passage, l'entree elle-meme designait le mauvais fichier. Ce sont deux fichiers distincts, pas un lien :
$ ls -l /boot/dietpi.txt /boot/firmware/dietpi.txt
-rw-r--r-- 1 root root 18518 Aug 27 10:25 /boot/dietpi.txt <- le VIVANT (rootfs, 222 references dans /boot/dietpi/)
-rwxr-xr-x 1 root root 18389 Mar 3 2025 /boot/firmware/dietpi.txt <- l'image d'origine (vfat, 0 reference)
Quatorze lignes different : la copie de l'image porte
AUTO_SETUP_SWAPFILE_SIZE=0 la ou la machine tourne a 2048, et
AUTO_SETUP_GLOBAL_PASSWORD=dietpi — le mot de passe par defaut, en clair, que
le fichier vivant a justement remplace par un commentaire.
Les deux sont corriges depuis le 2026-09-06 : le filtre teste -e, un chemin
declare et introuvable fait crier le script au lieu d'etre saute, et
CONFIG_PATHS designe desormais /boot/dietpi.txt. Meme motif que le cp
vers adguard/ plus haut : un chemin qui a l'air bon, une sauvegarde qui rend
vert, et rien du contenu attendu dedans.
Enfin, la couche 3 ne redemarre pas. Son gestionnaire le dit et s'arrete
la : dietpi-update a deja coupe une session par un postinst tailscale, et un
redemarrage automatique en fin de reconstruction empecherait de constater le
resultat. Redemarrer a la main, puis rejouer verify.yml.
Repli manuel — les phases 3bis a 6 a la main
Ce qui suit est l'ancien parcours manuel. Il reste ici pour une raison
precise, mesuree le 2026-09-06 : la couche 1 s'arrete sur 'paquets_base' is undefined des qu'on la vise sur penny. Le supprimer laisserait ce runbook sans
aucun chemin praticable pour la machine qu'il est cense reconstruire.
Il resterait utile de toute facon : la restauration des volumes depuis R2 et la configuration Authelia ne sont dans aucun playbook.
Phase 3bis — appliquer les configs systeme a la main. Equivalent manuel de
la couche 3. Les cp ci-dessous ecrasent sans regarder : c'est exactement
ce que le pre-vol du playbook existe pour empecher. Le 2026-08-27,
/boot/firmware/config.txt a recu dtoverlay=ramoops-pi4,... — le seul temoin
qui survive a un reset dur — et le depot ne l'a recu que le 2026-09-06.
Comparer avant de copier.
cd /mnt/ssd/config
diff boot/config.txt /boot/firmware/config.txt # LIRE avant d'ecraser
diff boot/cmdline.txt /boot/firmware/cmdline.txt
cp boot/config.txt /boot/firmware/config.txt
cp boot/cmdline.txt /boot/firmware/cmdline.txt
cp udev/50-argon-ssd.rules /etc/udev/rules.d/
udevadm control --reload-rules
cp system/fstab /etc/fstab # Adapter UUID si SSD different
cp system/97-dietpi.conf /etc/sysctl.d/
cp system/99-sysctl.conf /etc/sysctl.conf
cp system/crontab /var/spool/cron/crontabs/root
cp network/interfaces /etc/network/interfaces # IP 192.168.1.28
sysctl --system
Phase 4 (repli manuel) — Docker + services (T+45min a T+1h30)
Jusqu'à cette date, la boucle ci-dessous restaurait beszel et portainer. Or
homelab_backup.sh stage crowdsec, traefik-certs et adguard-runtime depuis
le 2026-04-17, sous des étiquettes qui ne sont pas les noms de volume. Suivie à la
lettre, la procédure ne restaurait ni les certificats TLS ni l'état CrowdSec.
La liste fait désormais foi dans VOLUMES= de scripts/homelab_backup.sh : la
relire avant d'exécuter, et ajuster la boucle si elle a changé.
Jusqu'à cette date, les blocs de restauration de cette page exportaient
B2_ACCOUNT_ID B2_ACCOUNT_KEY. Or /root/.restic-env ne contient plus ces
variables depuis la migration vers Cloudflare R2 du 2026-05-11 : il porte
AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY.
Et comme ce fichier utilise des assignations simples, sans export, un
source seul ne les transmet pas à restic, qui est un processus enfant. La
procédure échouait donc sur une erreur d'authentification — pendant une reprise
après sinistre, c'est-à-dire au pire moment.
Cassé pendant trois mois et demi sans que rien ne le signale : aucun chemin de code n'exécute cette page. Un runbook n'est vérifié que par un exercice de reprise qui le suit à la lettre.
Le correctif n'est pas de mettre à jour la liste des variables — ce serait le même
piège, reporté au prochain changement de backend. C'est set -a, qui auto-exporte
tout ce que le fichier assigne, sans rien coder en dur. C'est déjà ce que fait
vault-lxc-backup.sh en production depuis le début ; la page, elle, ne l'avait
jamais repris.
# Installer les paquets essentiels
dietpi-software install 162 # Docker
apt install -y fail2ban restic watchdog unattended-upgrades
# Configurer Docker
mkdir -p /etc/docker
cp /mnt/ssd/config/docker/daemon.json /etc/docker/daemon.json
systemctl restart docker
# Les configs applicatives sont DEJA en place : la phase 3 a clone le depot
# directement dans /mnt/ssd/config, qui EST le chemin lu par la production
# (bind-monte dans Traefik, en `watch: true`). Rien a recopier.
# (Corrige le 2026-09-06 : cette etape faisait trois `cp -r` depuis un clone
# voisin. L'un visait `adguard/`, chemin deplace vers `adguard/adguard-prod-1/`
# le 03/09 ; les deux autres seraient devenus des copies sur elles-memes.)
ls -d /mnt/ssd/config/adguard/adguard-prod-1 \
/mnt/ssd/config/traefik \
/mnt/ssd/config/homepage
# Les secrets applicatifs ont ete descelles en phase 3 par homelab-unseal.sh
# vers /run/homelab/ (tmpfs). Restent ceux de restic, hors de son perimetre.
# Execute le 2026-09-06 : rc=0, 8 assignations — RESTIC_REPOSITORY,
# RESTIC_PASSWORD, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, R2_ACCOUNT_ID,
# R2_BUCKET, R2_ENDPOINT, B2_CAP_BYTES.
# (Cette page a d'abord annonce « 4 clefs » : le compte venait d'un
# `grep -cE '^[A-Z_]+='`, qui ne matche pas un nom contenant un CHIFFRE et
# jetait donc les quatre entrees R2_/B2_. Compter avec
# `grep -cE '^[A-Za-z_][A-Za-z0-9_]*='`.)
SOPS_AGE_KEY_FILE=/root/.config/sops/age/keys.txt \
sops --input-type dotenv --output-type dotenv \
-d /mnt/ssd/config/system/secrets/restic-env.enc > /root/.restic-env
chmod 600 /root/.restic-env
# Le docker-compose est DEJA en place : le clone restaure le porte a
# docker/docker-compose.yml, qui est le chemin que lit la production.
# (Corrige le 2026-09-04 : cette etape faisait un `cp` vers la RACINE du depot,
# disposition d'avant le deplacement du compose sous docker/. Elle fabriquait
# un doublon au lieu de restaurer quoi que ce soit. Le chemin mort n'est PAS
# ecrit en entier ici : le controle de chemins vivants — section « Maintenance
# de ce document » — grep le texte de la page et ne sait pas distinguer une
# prescription d'un recit. Un controle qui signale toujours la meme chose
# n'informe plus.)
ls -l /mnt/ssd/config/docker/docker-compose.yml
# Restaurer les volumes Docker depuis restic (Cloudflare R2 EU, chiffre AES-256)
set -a; . /root/.restic-env; set +a # set -a auto-exporte : immune au prochain changement de backend
# Lister les snapshots disponibles
restic snapshots
# Restaurer les volumes stages dans le dernier snapshot
restic restore latest --target /tmp/restore --include "/mnt/ssd/.restic-staging"
# Injecter les donnees dans les volumes Docker.
# Paires volume:etiquette, recopiees de VOLUMES= dans scripts/homelab_backup.sh
# (l'etiquette est le nom du repertoire de staging, PAS le nom du volume).
grep -A8 '^VOLUMES=(' /mnt/ssd/config/scripts/homelab_backup.sh # verifier avant
for entry in docker_crowdsec-data:crowdsec \
config_traefik-certs:traefik-certs \
config_adguard-data:adguard-runtime; do
vol="${entry%%:*}"; label="${entry##*:}"
docker volume create "$vol" 2>/dev/null
docker run --rm \
-v "$vol":/data \
-v "/tmp/restore/mnt/ssd/.restic-staging/$label":/source:ro \
alpine sh -c "cp -a /source/. /data/"
echo "Restored: $vol (depuis $label)"
done
rm -rf /tmp/restore
Phase 5 (repli manuel) — Authelia (T+1h30 a T+1h45)
# Restaurer la config Authelia depuis restic ou regenerer les secrets
# (les configs bind-mount sont deja restaurees par restic dans /mnt/ssd/config/)
set -a; . /root/.restic-env; set +a # set -a auto-exporte : immune au prochain changement de backend
restic restore latest --target /tmp/restore --include "/mnt/ssd/config/authelia"
if [ -d /tmp/restore/mnt/ssd/config/authelia ]; then
cp -a /tmp/restore/mnt/ssd/config/authelia/. /mnt/ssd/config/authelia/
rm -rf /tmp/restore
echo "Authelia restauree depuis restic"
else
# Regenerer depuis les templates (cle USB pour les secrets)
cp authelia/configuration.yml.example /mnt/ssd/config/authelia/configuration.yml
cp authelia/users_database.yml.example /mnt/ssd/config/authelia/users_database.yml
# Generer les secrets
openssl rand -hex 32 # jwt_secret
openssl rand -hex 32 # session_secret
openssl rand -hex 32 # storage_key
openssl rand -hex 32 # hmac_secret
openssl genrsa -out /mnt/ssd/config/authelia/oidc.pem 4096
# ATTENTION : si on regenere oidc.pem, il faut reconfigurer
# les 6 clients OIDC (Proxmox, Grafana, PBS, Pulse, Forgejo, Outline)
echo "SECRETS REGENERES — mettre a jour configuration.yml manuellement"
fi
Phase 6 (repli manuel) — Réseau et vérification (T+1h45 a T+2h)
# Installer et configurer Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up --authkey=<TS_AUTHKEY_DEPUIS_CLE_USB>
# Ou se connecter via login.tailscale.com si pas d'authkey
# Deployer les scripts et les units — le manifeste Ansible, pas des `cp`.
# 109 paires source -> copie live sur les trois hotes ; `--check --diff`
# d'abord, LIRE le diff, puis appliquer. Voir la page « Derive de
# configuration ». Les `cp` manuels qui figuraient ici ne couvraient que deux
# fichiers sur les 45 scripts et 51 units que le manifeste de penny declare
# (compte le 2026-09-06 dans ansible/inventory/group_vars/penny.yml).
cd /mnt/ssd/config/ansible
ansible-playbook playbooks/deploy-scripts.yml --limit penny --check --diff
ansible-playbook playbooks/deploy-systemd.yml --limit penny --check --diff
# puis les deux memes sans --check
# Lancer la stack. Le projet s'appelle `config` (declare par `name:` en tete du
# compose) et non `docker` : c'est ce qui donne aux volumes leur prefixe
# `config_`, celui que la restauration ci-dessus recree.
cd /mnt/ssd/config/docker
docker compose up -d
# Verifier que restic fonctionne
set -a; . /root/.restic-env; set +a # set -a auto-exporte : immune au prochain changement de backend
restic snapshots # doit lister les snapshots existants
Vérification post-restauration penny
Le juge est verify.yml — il demande aux services de SERVIR, et il porte un
controle negatif. Le tableau ci-dessous couvre ce qu'il ne regarde pas
(materiel, ASPM, Tailscale, sauvegardes).
cd /mnt/ssd/config/ansible
ansible-playbook playbooks/verify.yml -e cible=penny
| Check | Commande | Attendu |
|---|---|---|
| SSD monte | mount | grep ssd | /mnt/ssd en rw |
| ASPM off | cat /sys/module/pcie_aspm/parameters/policy | [off] |
| DNS primaire | dig home.gabin-simond.fr @192.168.1.28 | 192.168.1.28 |
| Traefik HTTPS | curl -sk https://home.gabin-simond.fr | HTTP 200 ou redirect auth |
| Authelia | curl -sk https://auth.home.gabin-simond.fr | Page de login |
| Containers UP | docker ps --format "table {{.Names}}\t{{.Status}}" | Tous Up |
| Monitoring | cat /var/lib/homelab_monitor/heartbeat puis comparer a date +%s | Horodatage de moins de 15 min |
| Tailscale | tailscale status | Connecte au tailnet |
| Backup restic | set -a; . /root/.restic-env; set +a; restic snapshots > /tmp/s.txt | rc=0, snapshots recents dans /tmp/s.txt |
Scénario 2 : Cluster Proxmox detruit (galahad + lancelot)
Durée estimee : 2-3h | Impact : Vaultwarden, DNS secondaire, Grafana, Proxmox UI
Vérifier l'acces a l'app mobile Bitwarden (sync offline) avant de commencer. Les tokens critiques doivent aussi être sur la clé USB.
Phase 1 — Reinstaller Proxmox VE (T+0 a T+45min)
# Sur chaque ZimaBoard : boot depuis USB Proxmox VE 9
# IMPORTANT : patch eMMC avant installation
# Ctrl+Alt+F2 depuis l'installeur Proxmox, puis :
# (voir proxmox-fix-emmc.sh dans homelab-config/scripts/)
# Apres installation et reboot, appliquer le post-install depuis penny :
cd /mnt/ssd/config/scripts && python3 -m http.server 8888
# Sur galahad (192.168.1.18) :
wget -O- http://192.168.1.28:8888/proxmox-post-install.sh | bash
# Sur lancelot (192.168.1.19) :
wget -O- http://192.168.1.28:8888/proxmox-post-install.sh | bash
Phase 2 — Recreer le cluster (T+45min a T+1h)
# Sur galahad
pvecm create homelab
# Sur lancelot
pvecm add 192.168.1.18
# Mot de passe root de galahad requis
# Verifier
pvecm status
Phase 3 — Recreer les LXC (T+1h a T+2h)
- LXC 100 — dns-failover (galahad)
- LXC 102 — vault (galahad)
- LXC 101 — logs (lancelot)
# Telecharger le template Debian 12
pveam update
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
# Creer le conteneur
pct create 100 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname dns-failover --memory 256 --cores 1 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.30/24,gw=192.168.1.254 \
--unprivileged 1 --start 1
# Configurer
pct exec 100 -- bash -c "
apt update && apt install -y curl
# Installer AdGuard Home
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v
# Installer Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
"
# Restaurer la config AdGuard depuis le repo.
# Chemin corrige le 2026-09-04 : le renommage du 03/09 a deplace ce fichier
# vers adguard/adguard-prod-1/. L'ancien chemin etait MORT, et c'est l'etape
# qui restaure le resolveur DNS de secours — celle dont on a besoin quand tout
# le reste est a terre.
pct push 100 /mnt/ssd/config/adguard/adguard-prod-1/AdGuardHome.yaml \
/opt/AdGuardHome/AdGuardHome.yaml
pct exec 100 -- systemctl restart AdGuardHome
# Deployer le script health check (rpi_watchdog.sh)
pct create 102 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname vault --memory 256 --cores 1 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.32/24,gw=192.168.1.254 \
--unprivileged 1 --start 1
pct exec 102 -- bash -c "
apt update && apt install -y curl docker.io docker-compose
# Deployer Vaultwarden
mkdir -p /opt/vaultwarden
"
# Restaurer le volume Vaultwarden depuis R2
set -a; . /root/.restic-env; set +a # set -a auto-exporte : immune au prochain changement de backend
restic restore latest --target /tmp/restore --include "/mnt/ssd/.restic-staging/vaultwarden"
# Copier les donnees restaurees vers le LXC
pct push 102 /tmp/restore/mnt/ssd/.restic-staging/vaultwarden/ /opt/vaultwarden/data/ --recursive
rm -rf /tmp/restore
# Configurer le docker-compose pour Vaultwarden et demarrer
# Sur lancelot
pct create 101 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname logs --memory 512 --cores 2 \
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.31/24,gw=192.168.1.254 \
--unprivileged 1 --start 1
# Installer Loki + Grafana
# Reconfigurer OIDC Grafana avec Authelia (client_id: grafana)
# Les dashboards devront etre recrees (pas de backup des dashboards)
Phase 4 — Reconfigurer le SSO et les integrations (T+2h a T+2h30)
# Configurer Authelia OIDC pour Proxmox
pveum realm add authelia --type openid \
--issuer-url https://auth.home.gabin-simond.fr \
--client-id proxmox \
--client-key <SECRET_DEPUIS_VAULTWARDEN> \
--autocreate 1
# Token API pour Homepage
pveum user add homepage@pve
pveum aclmod / -user homepage@pve -role PVEAuditor
pveum user token add homepage@pve homepage --privsep=0
# Installer Tailscale sur les ZimaBoards
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
Vérification post-restauration cluster
| Check | Commande | Attendu |
|---|---|---|
| Cluster OK | pvecm status | Quorum OK, 2 nodes |
| LXC 100 UP | pct status 100 | running |
| LXC 101 UP | pct status 101 | running |
| LXC 102 UP | pct status 102 | running |
| DNS secondaire | dig home.gabin-simond.fr @192.168.1.30 | 192.168.1.28 |
| Vaultwarden | curl -sk https://vault.home.gabin-simond.fr | Page de login |
| OIDC Proxmox | Login via Authelia sur l'UI web | Session activé |
| Grafana | curl -sk https://logs.home.gabin-simond.fr | Page de login |
Scénario 3 : Tout detruit (penny + cluster)
Durée estimee : 4h + délai d'achat materiel
- YK1 ou YK2 avec son PIN (remplacé la clé USB)
age-plugin-yubikeyinstalle sur la machine de travail (brew install age-plugin-yubikey)- Acces au master password Vaultwarden (mémoire ou coffre papier)
- Acces au compte GitHub (pour cloner
homelab-config) — ou clone local - Clés SSH pour acceder aux machines (clé FIDO2 sur la YubiKey, ou clé Ed25519 backup)
Strategie : paralleliser les restaurations
Ordre de restauration
| Heure | penny | Cluster (galahad + lancelot) |
|---|---|---|
| T+0 | Achat/récupération du matériel — Flasher DietPi sur la SD card | — |
| T+0.5h | OS, configs, SSD (Phase 2-3 du Scénario 1) | Proxmox installer boot USB |
| T+1h | Docker up, restauration volumes depuis R2 | galahad : post-install.sh, création cluster |
| T+1.5h | Authelia restaurée, Traefik + DNS UP | lancelot : rejoint le cluster |
| T+2h | — | LXC 100 (dns-failover) UP, LXC 102 (vault) restauré depuis R2 |
| T+2.5h | — | LXC 101 (logs) Grafana + Loki, SSO reconfiguré (OIDC Proxmox/Grafana/PBS/Pulse) |
| T+3h | Tests end-to-end (voir checklist ci-dessous) | — |
| T+3.5h | Tailscale, vérification backup (restic snapshots) | — |
| T+4h | OPÉRATIONNEL | — |
Gestion de la dépendance d'amorçage (bootstrap)
Problème : Pour configurer Traefik, il faut le token Cloudflare. Ce token est dans Vaultwarden. Mais Vaultwarden est sur le cluster qu'on est aussi en train de reconstruire.
Solution (3 niveaux de fallback) :
- App mobile Bitwarden : synchro offline, disponible immédiatement
- Clé USB chiffrée : contient le
.envavecCF_DNS_API_TOKEN,TS_AUTHKEY, et tous les tokens - Coffre papier : master password Vaultwarden → acces via un client Bitwarden sur un autre appareil, mais nécessité que le LXC 102 soit déjà restaure
En pratique : déchiffrer .env.enc avec la YubiKey (age-plugin-yubikey --identity > /tmp/yk-id.txt && SOPS_AGE_KEY_FILE=/tmp/yk-id.txt sops --input-type dotenv -d .env.enc > .env) pour démarrer penny independamment du cluster.
Scénario 4 : Corruption Authelia (DB ou clé OIDC)
Durée estimee : 15-30 min | Impact : SSO indisponible, les services a ForwardAuth sont bloques
Vaultwarden n'utilisé pas Authelia. AdGuard utilisé ForwardAuth mais reste fonctionnel en acces direct (192.168.1.28:3000).
# Verifier les logs
docker logs authelia --tail 50
# Option A : Restaurer depuis le dernier backup
docker compose stop authelia
set -a; . /root/.restic-env; set +a # set -a auto-exporte : immune au prochain changement de backend
restic restore latest --target /tmp/restore --include "/mnt/ssd/config/authelia"
cp -a /tmp/restore/mnt/ssd/config/authelia/. /mnt/ssd/config/authelia/
rm -rf /tmp/restore
docker compose up -d authelia
# Option B : Regenerer les secrets (si backup corrompu aussi)
# ATTENTION : cela invalide TOUS les clients OIDC existants
cd /mnt/ssd/config
cp authelia/configuration.yml.example /mnt/ssd/config/authelia/configuration.yml
cp authelia/users_database.yml.example /mnt/ssd/config/authelia/users_database.yml
# Generer les 4 secrets + cle OIDC
for secret in jwt session storage hmac; do
echo "$secret: $(openssl rand -hex 32)"
done
openssl genrsa -out /mnt/ssd/config/authelia/oidc.pem 4096
# Regenerer les client secrets OIDC et les hasher
openssl rand -hex 32 # → secret clair pour le service
docker run --rm --entrypoint authelia authelia/authelia:latest \
crypto hash generate pbkdf2 --password "<SECRET_CLAIR>"
# Mettre le hash dans configuration.yml, le secret clair dans le service
# Regenerer le hash du mot de passe utilisateur
docker run --rm --entrypoint authelia authelia/authelia:latest \
crypto hash generate argon2 --password "<MOT_DE_PASSE>"
# Redemarrer et reconfigurer chaque client OIDC :
# - Proxmox : pveum realm modify authelia --client-key <NEW_SECRET>
# - Grafana : variable d'environnement GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
# - PBS, Pulse, Forgejo, Outline : secret client dans la config de chacun
# (liste a jour : clients OIDC de services/authelia.md)
docker compose up -d authelia
Scénario 5 : Perte d'acces réseau (FAI, DNS externe, Cloudflare)
Durée estimee : variable | Impact : pas d'acces distant, certificats TLS non renouvelables
# Diagnostic
ping 1.1.1.1 # Internet accessible ?
dig gabin-simond.fr @1.1.1.1 # DNS public fonctionne ?
curl -s https://api.cloudflare.com/ # Cloudflare accessible ?
tailscale status # Tailscale connecte ?
# Si Internet down :
# - Les services restent accessibles en LAN via IP directe
# - Les certificats TLS existants restent valides (90 jours Let's Encrypt)
# - Contacter le FAI (Free : 3244)
# Si Cloudflare down ou token revoque :
# - Generer un nouveau token API sur dashboard.cloudflare.com
# - Mettre a jour CF_DNS_API_TOKEN dans /mnt/ssd/config/.env
# - docker compose restart traefik
# Si Tailscale down :
# - Acces LAN uniquement
# - Re-authentifier : tailscale up --force-reauth
# - Si cle revoquee : login.tailscale.com > Machines > re-authorize
Scénario 6 : Perte de clé SSH ou YubiKey
Impact : acces SSH compromis ou bloque
# Si cle SSH compromise (vol/fuite) :
# 1. Retirer la cle de TOUTES les machines
for host in homelab pve1 pve2; do
ssh gabins@$host "sed -i '/<FINGERPRINT>/d' ~/.ssh/authorized_keys"
done
# 2. Generer une nouvelle paire
ssh-keygen -t ed25519 -C "homelab-$(date +%Y%m%d)"
# 3. Deployer la nouvelle cle
for host in homelab pve1 pve2; do
ssh-copy-id -i ~/.ssh/id_ed25519.pub gabins@$host
done
# Si YubiKey perdue/defaillante :
# 1. Se connecter avec TOTP (backup) sur Authelia
# 2. Supprimer la YubiKey defaillante dans Authelia (user settings)
# 3. Enregistrer la YubiKey de remplacement
# 4. Revoquer les sessions actives par precaution
Contacts critiques
| Provider | Raison | Contact |
|---|---|---|
| FAI (Free) | Panne Internet | 3244 |
| Cloudflare R2 | Acces backups | dashboard support |
| Proton Mail | Acces SMTP relay (token) | proton.me/support |
| Cloudflare | Zone DNS bloquee | dashboard support |
| Tailscale | Tailnet issue | [email protected] |
| YubiKey (Yubico) | Clé defaillante | support.yubico.com |
| DietPi | Bug OS | github.com/MichaIng/DietPi/issues |
Checklist de vérification finale
A exécuter après toute restauration, quel que soit le scénario :
#!/bin/bash
# break-glass-verify.sh — a lancer depuis penny
echo "=== VERIFICATION POST-RESTAURATION ==="
echo -n "SSD monte rw : "
mount | grep -q "/mnt/ssd.*rw" && echo "OK" || echo "FAIL"
echo -n "ASPM off : "
cat /sys/module/pcie_aspm/parameters/policy | grep -q "\[off\]" && echo "OK" || echo "FAIL"
echo -n "DNS primaire : "
dig +short home.gabin-simond.fr @192.168.1.28 | grep -q "192.168.1.28" && echo "OK" || echo "FAIL"
echo -n "DNS secondaire : "
dig +short home.gabin-simond.fr @192.168.1.30 | grep -q "192.168.1.28" && echo "OK" || echo "FAIL"
echo -n "Traefik HTTPS : "
curl -sko /dev/null -w "%{http_code}" https://home.gabin-simond.fr | grep -qE "200|302" && echo "OK" || echo "FAIL"
echo -n "Authelia : "
curl -sko /dev/null -w "%{http_code}" https://auth.home.gabin-simond.fr | grep -qE "200|302" && echo "OK" || echo "FAIL"
echo -n "Vaultwarden : "
curl -sko /dev/null -w "%{http_code}" https://vault.home.gabin-simond.fr | grep -qE "200|302" && echo "OK" || echo "FAIL"
echo -n "Tailscale : "
tailscale status >/dev/null 2>&1 && echo "OK" || echo "FAIL"
echo -n "Docker containers : "
total=$(docker ps -q | wc -l)
healthy=$(docker ps --filter "status=running" -q | wc -l)
echo "$healthy/$total running"
echo -n "Backup R2 accessible : " # R2 depuis le 2026-05-11, plus B2
set -a; . /root/.restic-env; set +a # `source` seul n'exporte rien vers restic
restic snapshots >/dev/null 2>&1 && echo "OK" || echo "FAIL"
echo -n "Temperature : "
temp=$(vcgencmd measure_temp | grep -oP '[0-9.]+')
echo "${temp}C $([ $(echo "$temp < 70" | bc) -eq 1 ] && echo 'OK' || echo 'WARN')"
echo "=== FIN ==="
Tests de la procédure
La procédure doit être testee au moins une fois par an sur un environnement jetable (spare RPi, VM locale, etc.).
| Date | Scénario | Durée reelle | Problèmes rencontres | Actions correctives |
|---|---|---|---|---|
| 2026-04-13 | DR drill Vaultwarden | 7s | Aucun | — |
| 2026-09-06 | Reconstruction complète sur banc jetable (LXC 111, couches 1+2) | 490 s le cycle complet (reset 58 + couche 1 316 + couche 2 105 + verdict 11), rejeu idempotent | Le banc n'est pas une Pi : couche 3 non eprouvable | verify.yml passe (ok=11 failed=0) ; couche 3 appuyee sur penny en --check --diff |
| — | Reconstruction complète penny (reelle, a froid) | — | A faire — voir les deux blocages mesures plus haut | — |
| — | Reconstruction cluster | — | A faire | — |
| — | Scénario 3 (tout detruit) | — | A faire | — |
| — | Rotation secrets Authelia | — | A faire | — |
Documenter chaque obstacle et chaque ecart par rapport au temps estime. Mettre a jour ce document après chaque test.
Maintenance de ce document
- Fréquence de révision : a chaque changement d'architecture (nouveau service, nouveau LXC, migration)
- Mise a jour clé USB : mensuelle (première semaine du mois)
- Test DR : annuel minimum, trimestriel recommandé
- Responsable : GabinSMD
Controle des chemins vivants — a rejouer apres chaque edition
Cette page a deja porte trois chemins morts, dont un dans l'etape qui restaure le resolveur DNS de secours — celle dont on a besoin quand tout le reste est a terre. Un chemin mort dans un runbook ne se voit pas : il attend l'incident.
cd /mnt/ssd/homelab-doc
# Chemins que la procedure CREE elle-meme, ou qui n'existent qu'au milieu d'une
# sauvegarde. Les declarer ici est une decision qu'on relit ; les taire dans le
# grep serait un filtre qui grandit tout seul sans que personne le voie.
ATTENDUS_ABSENTS='^/mnt/ssd/doc-repo$|^/mnt/ssd/\.restic-staging|^/mnt/ssd/config\.avant-reprise'
grep -oE '(/mnt/ssd|/boot|/etc|/root|/var/lib|/usr/local)/[A-Za-z0-9_./-]+' \
docs/operations/break-glass.mdx \
| sed 's|[.,;)]*$||' | sort -u \
| grep -Ev "$ATTENDUS_ABSENTS" \
| while read -r p; do
[ -e "$p" ] || printf ' MORT %s\n' "$p"
done
echo " (aucune ligne = tous vivants)"
La liste de racines a ete elargie le 2026-09-06, apres qu'une relecture eut
trouve un chemin mort dans le tableau de verification post-restauration — un
fichier d'etat du moniteur qui n'a jamais existe sous ce nom. La version
precedente du controle ne pouvait pas le voir : elle ne regardait que
/mnt/ssd/config. Un controle qui n'inspecte qu'une portion de son objet ne
dit rien du reste, et son silence ressemble a un succes.
Trois limites subsistent, a connaitre sinon il rassure a tort :
- les racines sont une liste, donc un chemin ailleurs echappe encore. En ajouter une plutot que de faire confiance a sa memoire ;
ATTENDUS_ABSENTSest une liste aussi, et elle vieillit dans l'autre sens : une entree dont le chemin a disparu de la page ne protege plus rien, elle masque juste un peu plus. La relire quand on l'allonge ;- il ne distingue pas une prescription d'un recit. Un chemin mort cite
pour raconter une correction passee sortirait en
MORTa chaque execution. D'ou la regle appliquee deux fois dans cette page : dans un commentaire historique, nommer le chemin abandonne sans l'ecrire en entier.
Compte rendu du drill du 2026-08-26
Premier exercice à suivre cette page. Non destructif : aucune LXC détruite, aucun service arrêté. Ce qui a été réellement exécuté est marqué comme tel.
Vérifié, et ça marche
- Les 15 fichiers que les phases 3 et 4 copient existent tous dans
homelab-config—boot/,udev/,system/,network/,docker/,adguard/,traefik/,homepage/, et les deux.enc. - La commande de restauration corrigée fonctionne. Exécutée telle qu'écrite :
les 4 variables attendues sont transmises, l'authentification à R2 passe,
restic snapshotsrenvoie les 12 snapshots. C'est la preuve que le correctifset -ade ce matin est réel et pas théorique. - Une restauration complète a été faite depuis R2 vers un répertoire jetable, puis comparée aux fichiers vivants : 15 fichiers, aucun écart. Les données sont récupérables, pas seulement présentes.
- La rétention est saine : mensuel, hebdomadaire, puis les sept derniers jours. Dernier snapshot du jour même.
restic checkpasse : 12 snapshots vérifiés,no errors were found. Le dépôt n'est pas seulement lisible, il est cohérent.
Trouvé pendant le drill
-
La dépendance circulaire GitHub — fermée le soir même par la copie hors-ligne chiffrée (voir l'encadré en tête de page). Reste un geste physique : copier l'archive sur la clé USB du coffre.
-
resticétait en 0.14.0, compilé en 2022 — monté en 0.19.1 le 26/08. Le paquet Debian de bookworm est figé en 0.14.0 : l'installation passe donc par le binaire officiel, posé viadpkg-divertpour qu'il occupe/usr/bin/restic(et non/usr/local/bin, que le PATH de cron ne contient pas) tout en survivant aux mises à jour du paquet. L'ancien binaire reste accessible en/usr/bin/restic.debian— retour arrière pardpkg-divert --remove --rename /usr/bin/restic.Vérifié avant de conclure : les 5 dépôts R2 listables,
restic checksans erreur surrestic-finance, aucun verrou laissé, et les deux endroits où les scripts lisent la sortie texte (et non--json) donnent toujours ce qu'ils attendent —restic stats … | grep "Total Size"danshomelab_backup.shet lerestic ls --longdu drill mensuel.
Deux pièges rencontrés pendant l'exercice
restic dans head ou tailLe SIGPIPE tue restic avant qu'il ne libère son verrou, et le dépôt reste
verrouillé. C'est arrivé pendant ce drill même, et c'est la mécanique exacte qui
avait laissé la sauvegarde Vaultwarden muette pendant six jours.
Rediriger vers un fichier, puis lire le fichier. Et avant de retirer un verrou,
vérifier que son PID est mort — kill -0 <pid> — plutôt que de dévérouiller par
réflexe.
restic snapshots --latest 1 n'est pas « le plus récent »L'option renvoie le dernier snapshot par groupe (hôte + chemins + tags), pas le sommet absolu. Lu trop vite pendant ce drill, ça donne l'impression que les sauvegardes sont arrêtées depuis dix-sept jours alors qu'elles tournent normalement. Pour l'âge réel : lister tout et trier.
Ce que ce drill n'a pas prouvé
La partie destructive. Recréer une LXC, réinstaller un OS, reconstruire le cluster : rien de tout ça n'a été exécuté. Les temps annoncés dans les objectifs de reprise restent donc des estimations, et le parcours complet n'est toujours pas chronométré.