Commit Graph
8 Commits
Author SHA1 Message Date
arcodangeandClaude Opus 5 01901e5903 Vault est resté scellé onze jours en silence, et c'était le pod le plus faible du cluster
Helm Charts / Detect changed charts (pull_request) Successful in 1m27s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts alloy (pull_request) Skipped
Helm Charts / Application charts chart (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Skipped
Helm Charts / Application charts grafana (pull_request) Skipped
Helm Charts / Application charts loki (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts hashicorp-vault (pull_request) Successful in 18s
Helm Charts / Application charts prometheus (pull_request) Successful in 20s
INCIDENT

Le 30/08 à 11:35, pi3 passe de 3,6 Gio de mémoire disponible à 1,7 en cinq
minutes. À 11:45 node-exporter ne répond plus, à 11:48 le nœud est NotReady, et
300 s plus tard — la tolérance `node.kubernetes.io/unreachable:NoExecute` — le
node-lifecycle controller supprime quatre pods, dont hashicorp-vault-0.

Vault repart SCELLÉ, comme le prévoit Shamir 1/1 sans auto-unseal. Personne ne
le redescelle. Pendant onze jours le VSO répond 503 « Vault is sealed » et plus
aucun Secret n'est réconcilié — découvert par hasard, en enquêtant sur autre
chose.

Ce qui a fonctionné ce jour-là : `NoeudInjoignable` a tiré à 11:46. L'infra a
parlé. Ce qui a manqué : la CONSÉQUENCE. Le nœud est revenu, les pods ont été
recréés, l'alerte s'est éteinte, et personne n'a su que Vault, lui, resterait
scellé.

Ce n'était ni un crash ni un OOMKill : `lastState: {}` et `restartCount: 0` sur
un pod vieux de 11 jours. Et le StatefulSet n'a aucune livenessProbe — seulement
une readiness `exec: vault status`, dont l'échec ne redémarre rien. D'où 196 906
events Unhealthy pour zéro redémarrage.

1. L'ALERTE QUI MANQUAIT — VaultIndisponible

`kube_pod_status_ready{namespace="tools", pod=~"hashicorp-vault-[0-9]+"} == 0`
pendant 10 min, avec `or absent(...)` pour couvrir la disparition du pod.

Pourquoi kube-state-metrics et pas `vault_core_unsealed` : Vault n'expose ses
métriques que via /v1/sys/metrics, qui exige un token ou l'ouverture d'un
endpoint non authentifié. Or la readinessProbe du chart est littéralement
`vault status` : un Vault scellé est NotReady. kube-state-metrics est déjà
scrapé, ça ne coûte rien et ça ne touche pas à la configuration de Vault.

⚠ Le sélecteur est ancré sur `[0-9]+`, et c'est une correction, pas un détail :
ma première version utilisait `hashicorp-vault-.*`, qui attrape aussi les pods
du Vault Secrets Operator — dont un exemplaire terminé traîne en permanence. La
règle tirait donc en continu. Vérifié contre le Prometheus du cluster : avec
`[0-9]+` elle ne rend que hashicorp-vault-0 et ne tire pas.

2. VAULT N'EST PLUS LE POD LE PLUS FAIBLE DU CLUSTER

Mesuré avant : `priorityClassName=""`, `priority=0`, QoS `BestEffort`. Dernier
servi par le scheduler, premier évincé par le kubelet. Sur 114 pods, 78 sont
BestEffort — Vault était noyé dedans, alors que tout le cluster dépend de lui
et qu'il exige une intervention humaine pour revenir.

- PriorityClass `vault-critical` à 900000000. Sous `longhorn-critical`
  (1000000000) parce que Vault stocke sur un PVC : le stockage doit survivre à
  Vault. Au-dessus de tout le reste.
- `requests: {memory: 512Mi, cpu: 100m}`. Vault consomme 175 Mio : très en
  dessous de sa requête, donc tout en bas de la liste d'éviction.

AUCUNE LIMITE, DÉLIBÉRÉMENT. Une limite trop basse déclenche un OOMKill, et un
OOMKill sur ce pod coûte un descellement manuel. Or je n'ai pas pu établir de
pic mémoire fiable : les séries cAdvisor de ce cluster rendent huit valeurs
contradictoires pour ce pod, jusqu'à 2,3 Gio, ce qui est invraisemblable pour un
Vault en storage "file". On ne pose pas une limite sur un chiffre auquel on ne
croit pas.

3. LE PDB EXISTANT : GARDÉ, MAIS SON PIÈGE EST MAINTENANT ÉCRIT

`maxUnavailable: 0` sur un StatefulSet à un réplica donne ALLOWED DISRUPTIONS: 0
en permanence. Un `kubectl drain` du nœud qui héberge Vault ne se terminera donc
jamais. C'est le comportement voulu — il force à traiter Vault consciemment —
mais la sortie de secours (`--disable-eviction`, ou suppression manuelle du pod)
n'était écrite nulle part. Elle l'est.

CE QUE ÇA NE RÉSOUT PAS, ET IL FAUT LE DIRE

Ni la priorité ni les requests n'empêchent la suppression par le node-lifecycle
controller quand un nœud devient injoignable — c'est exactement ce qui est
arrivé le 30/08, et aucun réglage de pod ne l'évite. Ce cas-là est traité par
l'alerte, pas par la protection.

L'auto-unseal reste écarté : décision assumée, documentée dans factory
vibe/guidebooks/lab-ecosystem/secrets-and-vault.md. Le descellement restera
manuel ; c'est le délai de détection qui passe de onze jours à dix minutes.

⚠ APPLICATION : le StatefulSet est en `updateStrategy: OnDelete`. Les réglages
du point 2 ne prendront effet qu'à la prochaine suppression du pod — laquelle
rescellera Vault. À faire au moment choisi, clé sous la main.

Vérifié : `helm template` rend la PriorityClass, le PDB et un StatefulSet
portant `priorityClassName: vault-critical` et les requests ; `helm lint` passe ;
la requête de l'alerte testée contre le Prometheus du cluster.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 12:56:19 +02:00
arcodange 5ac60c54fa fix(vault) — un PDB pour les évictions volontaires, OnRootMismatch pour les remounts
Helm Charts / Detect changed charts (pull_request) Successful in 1m59s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Ce Vault tourne en standalone (Shamir, un seul réplica, pas d'auto-unseal) :
toute interruption de pod exige un unseal humain ensuite. Le PDB du sous-chart
vendored ne se rend qu'en mode HA (server.ha.disruptionBudget, gardé par
`eq .mode "ha"`), donc jamais ici — il en fallait un écrit à la main.

Par ailleurs chaque remount du volume déclenchait un chown -R complet du
fsGroup, mesuré à ~9 min sur ce PVC (event kubelet
VolumePermissionChangeInProgress, suggestion du kubelet lui-même). Avec
OnRootMismatch, un remount dont le propriétaire racine est déjà correct saute
ce chown.

Le PDB ne protège que des évictions volontaires (drain, descheduler) — pas
d'une panne de nœud ni d'une suppression manuelle.
2026-08-11 13:15:13 +02:00
arcodangeandClaude Opus 5 2cb19809c6 doc(reseau) — le confort LAN ne se règle pas dans Traefik : mesure, pièges, voie retenue
Helm Charts / Detect changed charts (pull_request) Successful in 1m6s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Demande : « ne pas avoir le basic auth Traefik quand on est sur le Wi-Fi partagé
avec le homelab ». La réponse intuitive — une règle `ClientIP(192.168.1.0/24)` —
ne peut pas marcher, et la variante « je regarde CF-Connecting-IP » est un
contournement d'authentification. Ce commit écrit la mesure pour qu'on ne
re-découvre ni l'une ni l'autre.

Mesuré (2026-07-28, homelab) :

- `kadans.arcodange.fr` résout vers Cloudflare MÊME depuis le LAN, et redescend
  par le tunnel cloudflared. L'adresse de socket vue par Traefik est donc
  toujours celle d'un pod, en 10.42.x.x.
- Traefik SAIT qui est le vrai client (journal d'accès : l'IPv6 de la maison,
  `2a01:cb04:dff:cf00::/64` — pas l'IPv4 qu'`ipify` injecte dans `localIp`), car
  l'entrypoint `web` fait confiance aux en-têtes venant de 10.42.0.0/16.
- Mais le matcher de routeur `ClientIP()`, lui, juge la SOCKET. Prouvé par trois
  routeurs temporaires vers un Service sans endpoint (503 = règle matchée,
  401 = repli sur le routeur normal) : témoin 503, `ClientIP(<IPv6 maison>)` 401,
  `ClientIP(10.42.0.0/16)` 503. Sondes retirées après mesure.

D'où deux pièges, écrits là où on les rencontrerait :

- `localIp@file` sur un routeur `.fr` laisserait entrer Internet entier, parce
  que `10.42.0.0/16` est dans son `sourceRange` et qu'`ipAllowList` juge lui
  aussi la socket. Vérifié : aucun routeur `.fr` ne le porte aujourd'hui — le
  piège est latent, et les cinq endroits d'où on pourrait le recopier
  (2 gabarits `.fr`, 3 values `.lab`) portent désormais l'avertissement.
- Un en-tête posé par le client ne peut pas piloter une exemption d'auth :
  Cloudflare ne retire pas les en-têtes inconnus, et Traefik reste joignable en
  direct sur 192.168.1.201.

Voie retenue, et pourquoi celle-là : la décision « suis-je à la maison ? » doit
être prise là où la vraie IP est native et non falsifiable, donc au bord, chez
Cloudflare. Une règle de transformation y pré-remplit l'en-tête `Authorization`
pour les IP du foyer. Traefik ne bouge pas, aucun certificat public à produire,
aucun changement DNS. Surface d'attaque ajoutée : AUCUNE — `kadans:kkadans` est
déjà en clair dans le chart de kadans, Cloudflare ne fait que le taper à notre
place. Et quand l'IP du foyer dérive, la règle cesse de matcher : le navigateur
redemande le mot de passe. Dégradation douce, pas de panne.

Le doc écrit l'action Cloudflare mot pour mot (expression, en-tête, valeur, et
l'équivalent Terraform pour `cms/cloudflare`) plutôt que de la supposer faite,
avec les deux commandes qui la vérifient — et nomme le repli si Cloudflare
refuse de modifier `Authorization` : Cloudflare Access avec une politique de
bypass, PAS un en-tête secret.

Enfin, un contre-exemple utile : le bouncer CrowdSec, lui, juge la VRAIE IP
(journal à l'appui). Son `clientTrustedIPs` contient `10.42.0.0/16` sans que ce
soit un trou — noté pour que personne ne « corrige » cette ligne en croyant y
reproduire le piège.

⚠ Aucun des réglages en cause ne vit dans ce dépôt : les valeurs Helm de
Traefik, `dynamic.yaml`/`localIp` et le Middleware crowdsec sont dans `factory`
(ansible), le tunnel et le DNS dans `cms`. Ce dépôt reçoit la mesure et les
garde-fous parce que c'est lui qui héberge les gabarits `.fr` et les
consommateurs de `localIp@file`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
2026-07-28 14:29:35 +02:00
arcodange 02322e9a24 use internal .lab instead of failing duckdns.org
Helm Charts / Detect changed charts (push) Successful in 22s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Failing after 34s
2025-12-31 17:54:36 +01:00
arcodange bbb0bc7d5f configure vault secrets operator 2024-10-30 11:21:48 +01:00
arcodange 3a506543ce apply vault config from CI 2024-10-05 23:58:24 +02:00
arcodange 7a640d104c experiment with vault configuration 2024-09-27 12:22:36 +02:00
arcodange 042e5b8767 add hashicorp vault 2024-09-05 19:45:42 +02:00