From 01901e590327441fb7b39f983f05415bd98ebaff Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 11 Sep 2026 12:56:19 +0200 Subject: [PATCH] =?UTF-8?q?Vault=20est=20rest=C3=A9=20scell=C3=A9=20onze?= =?UTF-8?q?=20jours=20en=20silence,=20et=20c'=C3=A9tait=20le=20pod=20le=20?= =?UTF-8?q?plus=20faible=20du=20cluster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- hashicorp-vault/templates/priorityclass.yaml | 33 +++++++++++ .../templates/vault-server-pdb.yaml | 11 ++++ hashicorp-vault/values.yaml | 30 ++++++++++ prometheus/values.yaml | 55 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 hashicorp-vault/templates/priorityclass.yaml diff --git a/hashicorp-vault/templates/priorityclass.yaml b/hashicorp-vault/templates/priorityclass.yaml new file mode 100644 index 0000000..1401db1 --- /dev/null +++ b/hashicorp-vault/templates/priorityclass.yaml @@ -0,0 +1,33 @@ +{{/* + F—: VAULT NE DOIT PLUS ÊTRE LE POD LE PLUS FAIBLE DU CLUSTER. + + Constat du 2026-09-11, mesuré : `priorityClassName=""`, `priority=0`, QoS `BestEffort`. + Autrement dit Vault était littéralement dernier servi et premier évincé, alors que le + Vault Secrets Operator en dépend pour TOUS les Secrets du cluster. Quand il tombe, il + repart scellé (Shamir 1/1, pas d'auto-unseal — décision assumée) et plus rien ne se + réconcilie jusqu'à ce qu'un humain vienne le desceller. + + POURQUOI 900000000. Il existe déjà `longhorn-critical` à 1000000000. Vault stocke ses + données sur un PVC : si Longhorn tombe, Vault ne repart pas. Le stockage doit donc rester + AU-DESSUS. En dessous de Vault, tout le reste — applications comprises. + + CE QUE ÇA ACHÈTE, ET CE QUE ÇA N'ACHÈTE PAS. La priorité joue sur la préemption par le + scheduler et sert de critère de tri à l'éviction par le kubelet sous pression. Elle + n'empêche PAS la suppression décidée par le node-lifecycle controller quand un nœud + devient injoignable — c'est exactement ce qui s'est produit le 30/08 et aucun réglage de + pod ne l'évite. Ce qui traite ce cas-là, c'est l'alerte VaultIndisponible. +*/}} +apiVersion: scheduling.k8s.io/v1 +kind: PriorityClass +metadata: + name: vault-critical + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: hashicorp-vault +value: 900000000 +globalDefault: false +preemptionPolicy: PreemptLowerPriority +description: >- + Vault et son opérateur : tout le cluster en dépend pour ses Secrets, et un Vault tombé + repart scellé, donc muet jusqu'à intervention humaine. Sous longhorn-critical (le + stockage doit survivre à Vault), au-dessus de tout le reste. diff --git a/hashicorp-vault/templates/vault-server-pdb.yaml b/hashicorp-vault/templates/vault-server-pdb.yaml index 827304a..96c889a 100644 --- a/hashicorp-vault/templates/vault-server-pdb.yaml +++ b/hashicorp-vault/templates/vault-server-pdb.yaml @@ -8,6 +8,17 @@ # d'une panne de nœud ni d'une suppression manuelle. Avec un seul réplica et # aucun auto-unseal, toute interruption exige un unseal humain ensuite ; ce PDB # réduit seulement les interruptions évitables. +# +# ⚠ CONSÉQUENCE À CONNAÎTRE AVANT LA PROCHAINE MAINTENANCE D'UN NŒUD. +# `maxUnavailable: 0` sur un StatefulSet à UN réplica donne `ALLOWED DISRUPTIONS: 0` +# en permanence — vérifié : `kubectl get pdb -n tools` le montre depuis 20 jours. +# Un `kubectl drain ` sur le nœud qui héberge Vault ne se terminera donc JAMAIS, +# il bouclera en silence sur « cannot evict pod as it would violate the budget ». +# C'est le comportement voulu — il force à traiter Vault consciemment — mais il faut +# savoir en sortir : +# kubectl drain --disable-eviction # contourne le PDB (supprime le pod) +# ou kubectl delete pod -n tools hashicorp-vault-0 # puis descellement à la main +# Dans les deux cas Vault repart scellé : avoir la clé sous la main AVANT de drainer. apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/hashicorp-vault/values.yaml b/hashicorp-vault/values.yaml index 4442154..6ee3247 100644 --- a/hashicorp-vault/values.yaml +++ b/hashicorp-vault/values.yaml @@ -6,6 +6,36 @@ vault: &vault_config server: enabled: true logLevel: trace + # ---- NE PLUS ÊTRE LE POD LE PLUS FAIBLE DU CLUSTER ----------------------------- + # Mesuré le 2026-09-11, avant ce changement : `priorityClassName=""`, `priority=0`, + # QoS `BestEffort`. Vault était donc dernier servi par le scheduler et PREMIER évincé + # par le kubelet sous pression mémoire — alors que le Vault Secrets Operator en dépend + # pour tous les Secrets du cluster, et qu'un Vault tombé repart SCELLÉ, donc muet + # jusqu'à ce qu'un humain vienne le desceller à la main. + # + # Contexte : 78 pods du cluster sont BestEffort contre 35 Burstable et 1 Guaranteed. + # Vault était noyé dans le premier groupe. + priorityClassName: vault-critical # cf. templates/priorityclass.yaml + resources: + # DES REQUESTS, ET DÉLIBÉRÉMENT AUCUNE LIMITE. + # + # Ce sont les `requests` qui protègent : le kubelet évince d'abord les BestEffort, + # puis les Burstable qui DÉPASSENT leur requête, et en dernier ceux qui restent + # dessous. Vault consomme ~175 Mio (kubectl top, 11/09) : avec 512 Mio demandés il + # est très en dessous, donc tout en bas de la liste des candidats à l'éviction. + # + # Pourquoi pas de `limits.memory`, qui donnerait du Guaranteed : 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 de 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 : le gain marginal ne vaut pas le risque. + # + # Pourquoi pas de `limits.cpu` : le throttling CPU sur un ARM64 à 4 cœurs ralentirait + # les opérations de descellement et de scellement, pour un pod qui consomme 26 m. + requests: + memory: 512Mi + cpu: 100m auditStorage: enabled: true diff --git a/prometheus/values.yaml b/prometheus/values.yaml index 05f2f47..0f85ba7 100644 --- a/prometheus/values.yaml +++ b/prometheus/values.yaml @@ -1265,6 +1265,61 @@ prometheus: &prometheus_config annotations: summary: "Homelab — cert {{ $labels.namespace }}/{{ $labels.name }} expire dans {{ $value | humanizeDuration }}" description: "Le renouvellement automatique (cert-manager → step-issuer → step-ca) est en échec. Vérifier : kubectl get certificaterequest -A, logs step-issuer (résolution DNS de ssl-ca.arcodange.lab), santé de step-ca sur pi1:8443." + # ---- VAULT SCELLÉ ------------------------------------------------------ + # Incident 2026-08-30 → 2026-09-10 : Vault est resté SCELLÉ ONZE JOURS. + # Le VSO ne pouvait plus s'authentifier (503 « Vault is sealed »), donc les + # Secrets de plusieurs applications ont cessé d'être réconciliés — en silence. + # Découvert par hasard, en enquêtant sur tout autre chose. + # + # CE QUI A MARCHÉ CE JOUR-LÀ : `NoeudInjoignable` a bien tiré à 11:46 quand pi3 + # a cessé de répondre. 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, était reparti scellé et le resterait. + # + # POURQUOI `kube_pod_status_ready` ET PAS `vault_core_unsealed` : Vault n'expose + # ses métriques que via /v1/sys/metrics, qui exige soit un token, soit + # `unauthenticated_metrics_access = true` dans la stanza telemetry. Le second + # ouvre un endpoint non authentifié pour gagner… le même signal. Or la + # readinessProbe du chart est littéralement `vault status` : un Vault scellé est + # NotReady, point. kube-state-metrics est déjà scrapé, ça ne coûte rien, et ça + # ne touche pas à la configuration de Vault. + # + # ⚠ Cette alerte ne distingue pas « scellé » de « en train de démarrer » ou + # « planté ». C'est VOULU : dans les trois cas Vault ne sert plus de secrets, et + # le geste de l'opérateur commence pareil — aller voir `vault status`. + - alert: VaultIndisponible + # ⚠ LE SÉLECTEUR EST ANCRÉ SUR `[0-9]+`, ET CE N'EST PAS COSMÉTIQUE : un + # `hashicorp-vault-.*` attrape aussi les pods du Vault Secrets Operator + # (`hashicorp-vault-vault-secrets-operator-controller-manager-xxxxx`), dont un + # exemplaire terminé traîne en permanence — la règle aurait donc tiré sans + # arrêt, et une alerte qui crie tout le temps ne se lit plus. Vérifié contre le + # Prometheus du cluster : `[0-9]+` ne rend que hashicorp-vault-0. Toute + # évolution de ce sélecteur se re-teste de la même façon, sur le vrai serveur. + # + # `absent()` : si le pod disparaît (évincé, nœud perdu), la série n'existe plus + # et une simple comparaison `== 0` serait muette — exactement le mode de panne + # qu'on essaie de couvrir. Même motif que IngressLabIndisponible ci-dessus. + expr: >- + kube_pod_status_ready{namespace="tools", pod=~"hashicorp-vault-[0-9]+", condition="true"} == 0 + or absent(kube_pod_status_ready{namespace="tools", pod=~"hashicorp-vault-[0-9]+", condition="true"}) + # 10 min : large devant un redémarrage normal (le pod est Ready en ~30 s), + # court devant onze jours. + for: 10m + labels: + severity: critical + app: homelab + annotations: + summary: "Homelab — Vault ne sert plus de secrets (probablement scellé)" + description: >- + hashicorp-vault n'est plus Ready depuis 10 min. Le cas le plus fréquent est + un Vault SCELLÉ : il repart toujours scellé après recréation du pod (Shamir + 1/1, pas d'auto-unseal — c'est une décision assumée, cf. factory + vibe/guidebooks/lab-ecosystem/secrets-and-vault.md). Tant qu'il l'est, le + Vault Secrets Operator ne réconcilie plus AUCUN Secret : les applications + tournent sur leurs Secrets existants et cassent dès qu'un pod redémarre. + Vérifier - kubectl -n tools exec hashicorp-vault-0 -- vault status. + Desceller - le rôle ansible arcodange.factory/hashicorp_vault, tâche + unseal.yml (clé dans ~/.arcodange/cluster-keys.json sur le poste). # Le veilleur (arcodange-org/tools#36). Du 2026-09-04 02:41 au # 2026-09-07 07:37, Prometheus n'a rien enregistré et les 11 règles # ci-dessus se sont TUES — sans échantillon frais, une règle ne se -- 2.54.0