Skip to content

Sprint 6 — Multi-environnements

Incidents du Sprint 6 : exposition et cloisonnement des environnements dev / staging / prod.


SEC-006 : dev et staging publiquement exposés en clair, branchés sur la base de production

SĂ©vĂ©ritĂ© : Critical Statut : ✅ RĂ©solu — correctif #146 mergĂ© et validĂ© live le 2026-07-26 (MR !258)

Contexte : DĂ©couvert le 2026-07-26 pendant la session de validation live de #136, en rĂ©pondant Ă  une question sur les certificats — donc totalement hors du pĂ©rimĂštre de la tĂąche en cours. Le multi-env structurel (#134) Ă©tait en place depuis le 2026-07-25, mais le socle d'isolation ne l'Ă©tait pas : #137 (databases sĂ©parĂ©es) et #138 (secrets / DNS / certificats par env) Ă©taient encore ouverts. Une consigne de pilotage disait « ne pas remonter le multi-env en live avant la fin du socle ».

Fichiers : k8s/overlays/{fastapi,frontend}/{dev,staging}/kustomization.yaml, k8s/base/externalsecret.yaml, k8s/platform/gateway.yaml

SymptĂŽme : curl http://dev.api.devopsyouss.com/healthz/ready → 200, en clair, depuis n'importe oĂč sur internet. Idem pour staging.api, dev.app et staging.app.

Cause : Trois briques qui font chacune correctement leur travail, et dont la composition produit un rĂ©sultat que personne n'a demandĂ©. (1) Le Gateway partagĂ© expose deux listeners, :80 et :443, tous deux en allowedRoutes: from: All — nĂ©cessaire, sinon le multi-env cross-namespace ne fonctionne pas. (2) Aucune HTTPRoute du dĂ©pĂŽt ne portait de sectionName : or une HTTPRoute sans sectionName s'attache Ă  tous les listeners que le Gateway lui ouvre, pas seulement au bon. (3) ExternalDNS (source gateway-httproute) publie tout hostname rencontrĂ©, sans notion d'« environnement pas encore prĂȘt ». Aggravant : les overlays dev et staging ne surchargent pas l'ExternalSecret — le base tire la clĂ© fastapi-eks/app depuis AWS Secrets Manager, la mĂȘme pour les trois envs, donc dev et staging tapent la base de production. Et le tag de dev est bumpĂ© automatiquement par le write-back Ă  chaque merge sur develop : du code n'ayant passĂ© ni staging ni E2E servait publiquement sur les donnĂ©es de prod. Le risque d'intĂ©gritĂ© prĂ©cĂ©dait le risque d'intrusion.

SymptĂŽme trompeur qui a failli faire classer l'affaire : la premiĂšre alerte n'Ă©tait pas le 200 en clair, c'Ă©tait une erreur de certificat sur https://dev.api.devopsyouss.com (le wildcard *.devopsyouss.com ne couvre qu'un seul niveau de label, donc pas dev.api.). On pouvait en conclure « dev est protĂ©gĂ© ». Faux : un certificat invalide n'est pas un contrĂŽle d'accĂšs, c'est le client qui dĂ©cide de l'accepter — et il suffisait d'enlever le s de https pour tout contourner sans le moindre avertissement.

Fix (#146, MR !258) : les 4 overlays de dev et staging suppriment la HTTPRoute hĂ©ritĂ©e du base ($patch: delete) au lieu de lui donner un hostname. Sans route, rien ne s'attache au Gateway et ExternalDNS ne publie aucun enregistrement. Les envs restent joignables par leur Service ClusterIP et par kubectl port-forward. Alternative Ă©cartĂ©e : garder les routes et restreindre l'accĂšs (allowlist IP, basic auth) — le problĂšme n'est pas « qui accĂšde Ă  dev » mais « dev Ă©crit dans la base de prod », donc une allowlist aurait rĂ©duit le nombre de personnes capables de corrompre les donnĂ©es sans supprimer le chemin, tout en ajoutant une configuration Ă  maintenir puis Ă  retirer.

Validation : MergĂ© puis synchronisĂ© par ArgoCD le 2026-07-26. kubectl get httproute -A ne renvoie plus que 4 routes, toutes en prod ou plateforme (fastapi-prod, frontend-prod, monitoring, argocd) — aucune dans les 4 namespaces dev/staging. Preuve la plus forte cĂŽtĂ© ExternalDNS, qui dĂ©rive ses enregistrements des HTTPRoutes : action=DELETE du CNAME et des deux TXT de propriĂ©tĂ© (<host> et cname-<host>, marque txtOwnerId=fastapi-eks) pour les 4 hosts, en deux passes de rĂ©conciliation. Non-rĂ©gression prod : curl https://api.devopsyouss.com/healthz/ready → {"status":"ok"}. AccĂšs lĂ©gitime prĂ©servĂ© : port-forward sur fastapi-dev → {"status":"ok"}. ConfirmĂ© une seconde fois le 2026-07-28 sur un cluster montĂ© de zĂ©ro — cas qui n'avait jamais Ă©tĂ© exercĂ©, la premiĂšre validation portant sur un cluster dĂ©jĂ  en place : aprĂšs bootstrap complet et synchronisation ArgoCD, kubectl get httproute -A ne montre aucune route dans les 4 namespaces dev/staging. L'exposition ne se reproduit donc pas au dĂ©marrage, qui Ă©tait prĂ©cisĂ©ment le chemin par lequel elle s'Ă©tait produite.

Leçon : Un garde-fou documentaire n'est pas un contrĂŽle. La consigne « ne pas remonter le multi-env en live avant la fin du socle » existait, et n'a servi Ă  rien : elle n'Ă©tait Ă©crite nulle part oĂč elle s'appliquait. L'exposition s'est faite toute seule au dĂ©marrage du cluster, sans qu'aucun humain ait Ă  agir — ArgoCD synchronise, les routes s'attachent, ExternalDNS publie. C'est la deuxiĂšme occurrence du mĂȘme schĂ©ma sur ce projet, aprĂšs le gate prod « documentaire » (CODEOWNERS en place mais l'approbation obligatoire est payante, donc il suggĂšre sans bloquer). Deux instances indĂ©pendantes, ce n'est plus une anecdote : une rĂšgle qui dĂ©pend de la mĂ©moire d'un humain au bon moment est un vƓu, pas un contrĂŽle. Quand on Ă©crit « il ne faut pas faire X », il faut aussitĂŽt se demander qu'est-ce qui empĂȘche X ? — sans rĂ©ponse, la note ne protĂšge rien. Correctif de fond encore Ă  venir : #137 et #138. Voir le guide Exposition involontaire et l'ADR 029.


SEC-007 : aucune redirection HTTP vers HTTPS, identifiants transmis en clair (production incluse)

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — correctif #147 mergĂ© (MR !257) et validĂ© live le 2026-07-28 sur un cluster montĂ© de zĂ©ro

Contexte : Découvert le 2026-07-26 en instruisant SEC-006. Le défaut est antérieur au multi-env : il touche la production, publique depuis le Sprint 4. Le multi-env n'a fait que multiplier la surface, de 2 endpoints à 6.

Fichiers : k8s/base/httproute.yaml, k8s/frontend/httproute.yaml, k8s/platform/argocd/httproute-argocd.yaml, k8s/platform/monitoring/httproute-grafana.yaml, k8s/platform/gateway.yaml

SymptĂŽme : curl http://api.devopsyouss.com/healthz/ready → 200. Les 4 services publics (api, app, grafana, argocd) rĂ©pondent en clair aussi bien qu'en TLS.

Cause : MĂȘme mĂ©canisme que SEC-006 — aucune HTTPRoute ne porte de sectionName, donc toutes s'attachent aux deux listeners du Gateway. Aucun RequestRedirect, aucun ClientTrafficPolicy nulle part dans le dĂ©pĂŽt. Le TLS existait et fonctionnait ; rien n'obligeait le client Ă  l'emprunter. C'est une faille et pas un dĂ©tail de configuration : POST /login accepte identifiants et mot de passe et renvoie un JWT (app/routers/auth.py), et la CLI argocd transporte un token de session. Sur http://, les deux sont lisibles par tout observateur du rĂ©seau. CWE-319, Cleartext Transmission of Sensitive Information.

Fix (#147, MR !257) : deux piĂšces qui ne jouent pas le mĂȘme rĂŽle. (1) sectionName: https sur les 4 routes applicatives — c'est ceci qui ferme le clair, en les retirant du listener :80. (2) Une HTTPRoute de redirection (k8s/platform/httproute-https-redirect.yaml) sur sectionName: http, sans hostnames donc attrape-tout, avec un filtre RequestRedirect en 301 — elle ne ferme rien, elle offre une sortie propre plutĂŽt qu'un 404 sec, et le 301 est mis en cache par le navigateur. La redirection n'a ni hostname ni backend (un RequestRedirect rĂ©pond au niveau du Gateway), elle vit donc dans le namespace plateforme fastapi sans y avoir de Service. PosĂ©e par le bootstrap Ansible et non en GitOps (ADR 010) : elle dĂ©crit le comportement d'un listener, elle appartient Ă  la plomberie du Gateway.

Validation : Hors cluster d'abord — kube-linter sans erreur, kubectl kustomize sur les 6 overlays (le sectionName du base coexiste avec le patch parentRefs/0/namespace des overlays), mkdocs build. ValidĂ©e en live le 2026-07-28, sur un cluster montĂ© de zĂ©ro aprĂšs merge, ce qui Ă©tait le point non trivial : la redirection n'Ă©tant pas en GitOps, le merge seul ne prouvait rien sur son dĂ©ploiement.

Les 4 services publics renvoient 301 -> https://<host>/ (api, app, grafana, argocd), lĂ  oĂč ils rĂ©pondaient tous 200 en clair le 2026-07-26. Non-rĂ©gression du TLS : curl https://api.devopsyouss.com/healthz/ready → {"status":"ok"}. Cause racine fermĂ©e et pas seulement son symptĂŽme — kubectl get httproute -A ne montre plus aucune route sans sectionName :

argocd          argocd           https
fastapi-prod    fastapi          https
fastapi         https-redirect   http
frontend-prod   frontend         https
monitoring      grafana          https

Preuve du chemin de dĂ©ploiement : kubectl get httproute https-redirect -n fastapi renvoie bien la route, sans hostname, posĂ©e par le bootstrap Ansible au montage, sans intervention manuelle. ⚠ PiĂšge de dĂ©ploiement, toujours valable : sur un cluster dĂ©jĂ  montĂ©, un merge ne suffit pas — il faut rejouer le bootstrap ou appliquer le fichier Ă  la main.

Leçon : Le chiffrement disponible n'est pas le chiffrement imposĂ©. Un listener ouvert que personne n'a dĂ©cidĂ© d'utiliser reste un chemin exploitable, et il suffit d'un caractĂšre en moins dans une URL pour l'emprunter. Corollaire d'architecture propre Ă  Gateway API : un parentRef sans sectionName est un attachement implicite Ă  tout — c'est un dĂ©faut par omission, invisible Ă  la relecture d'un manifeste pris isolĂ©ment, et il faut le chercher activement. Point de vigilance associĂ© : le :80 ne doit dĂ©sormais plus servir Ă  rien d'autre ; les certificats passent par DNS-01, donc aucun challenge HTTP-01 Ă  prĂ©server, mais un futur ClusterIssuer HTTP-01 serait aveuglĂ© par la route attrape-tout. Voir le guide Exposition involontaire et l'ADR 017 DĂ©cision 4.


INC-062 : la rétention ECR a supprimé l'image servie par staging et prod

SĂ©vĂ©ritĂ© : High Statut : ⚠ RĂ©cidive le 2026-08-12, voir INC-065 — la rĂ©tention Ă©largie (5 → 30) n'a que retardĂ© l'Ă©chĂ©ance, la cause de fond est rouverte en #150

Contexte : Découvert le 2026-07-28 en cherchant pourquoi le schedule registry-scan (Phase 1 vuln-mgmt, #118) échouait tous les matins depuis le 2026-07-27. Le job était rouge depuis trois exécutions consécutives sans que personne ne l'ait relié au passage en multi-env du 2026-07-25. Le cluster étant éteint, l'impact réel restait invisible.

Fichiers : terraform/modules/ecr/main.tf, terraform/persistent/variables.tf, k8s/overlays/{fastapi,frontend}/{staging,prod}/kustomization.yaml

SymptĂŽme : Le job registry-scan Ă©choue avec MANIFEST_UNKNOWN: Requested image not found sur deployed-de3dcf4e..., le tag rĂ©fĂ©rencĂ© par l'overlay de prod. L'inventaire ECR confirme qu'il ne reste que 5 tags deployed-, tous poussĂ©s dans la mĂȘme soirĂ©e du 2026-07-26 entre 17h15 et 20h41. L'image de prod n'existe plus, ni dans fastapi-eks/fastapi, ni dans fastapi-eks/frontend.

Cause : La lifecycle policy ECR conserve les N derniĂšres images portant le prĂ©fixe deployed- (deployed_retention_count, alors Ă  5). Le dĂ©coupage deployed- / candidate- mis en place en #104 protĂšge l'image servie contre la rĂšgle d'Ăąge des candidates, mais pas contre le count : les trois environnements rĂ©fĂ©rencent des tags du mĂȘme moule et concourent donc dans la mĂȘme fenĂȘtre de rĂ©tention. Or leurs cadences sont opposĂ©es. Dev est bumpĂ© automatiquement Ă  chaque merge sur develop (write-back reciblĂ© en #139), tandis que staging et prod restent figĂ©s entre deux promotions dĂ©libĂ©rĂ©es (scripts/promote.sh, ADR 029 D2). Le 2026-07-26 au soir, cinq merges en trois heures et demie ont suffi Ă  faire sortir l'image de prod de la fenĂȘtre. Aggravant : comme la rĂšgle 1 rĂ©clamait cette image, son expiration a emportĂ© l'image entiĂšre, donc aussi son tag candidate- jumeau — il n'y a rien Ă  retaguer, le digest a disparu.

Fix (#150) : deployed_retention_count portĂ© de 5 Ă  30, puis terraform apply sur le stack persistent (appliquĂ© en local, la CI infra ne gĂšre que terraform/ephemeral). Il faut corriger la rĂ©tention avant de promouvoir, sinon les deux merges de promotion consomment eux-mĂȘmes deux places et l'image fraĂźchement promue se retrouve Ă  trois merges de l'Ă©jection. Staging et prod sont ensuite repointĂ©s sur deployed-e9f2fd64..., seule cible saine encore prĂ©sente, par deux MR de promotion successives (dev vers staging, puis staging vers prod). Solution Ă©cartĂ©e pour l'instant : marquer l'image Ă  la promotion avec un prĂ©fixe dĂ©diĂ© (inuse-<env>-<sha>) protĂ©gĂ© par une rĂšgle en prioritĂ© 1. C'est le seul fix qui ferme rĂ©ellement le trou, mais il impose un nouveau job CI qui pose un tag ECR au merge de la MR de promotion, promote.sh ne parlant qu'Ă  Kustomize. Un tag mobile type inuse-prod est impossible, les deux repos sont en IMMUTABLE.

Validation : Faite en live le 2026-07-28 au montage du cluster. Les six workloads démarrent, aucun ImagePullBackOff, et kubectl get events -A --field-selector reason=Failed | grep -i pull ne renvoie rien. Les images réellement servies correspondent aux overlays :

fastapi-prod     
/fastapi:deployed-340a8e92

fastapi-staging  
/fastapi:deployed-340a8e92

frontend-prod    
/frontend:deployed-340a8e92

frontend-staging 
/frontend:deployed-340a8e92

fastapi-dev      
/fastapi:deployed-905509fb

frontend-dev     
/frontend:deployed-905509fb


RĂ©tention vĂ©rifiĂ©e cĂŽtĂ© AWS aprĂšs apply : countNumber = 30 sur la rĂšgle 1 des deux dĂ©pĂŽts. Le compteur de tags deployed- est passĂ© de 5 Ă  7 pendant la session — sous l'ancienne valeur, les merges de la journĂ©e auraient supprimĂ© deux images de plus. Reste Ă  confirmer : registry-scan ne pourra repasser au vert qu'au prochain schedule du matin avec le ci_config_path rebasculĂ© sur l'application. Le tag qu'il cherche, deployed-340a8e92, est prĂ©sent dans les deux dĂ©pĂŽts.

Leçon : Un mĂ©canisme de protection reste valide tant que ses hypothĂšses le sont. Le dĂ©coupage de #104 Ă©tait correct en mono-environnement, oĂč l'image servie Ă©tait toujours la derniĂšre produite, donc structurellement en tĂȘte de fenĂȘtre et jamais menacĂ©e par un count. Le passage en multi-env a supprimĂ© cette propriĂ©tĂ© sans que personne ne relise la rĂšgle, parce que le changement portait sur Kustomize et ArgoCD, pas sur Terraform. La revue d'impact d'un changement d'architecture doit descendre jusqu'aux garde-fous Ă©crits pour l'architecture prĂ©cĂ©dente, y compris ceux qui vivent dans une autre couche de la stack. Corollaire opĂ©rationnel : une politique de rĂ©tention de registre ne sait pas ce qui tourne dans le cluster, elle ne connaĂźt que des dates et des compteurs. Tant qu'aucun tag ne distingue l'image en service, Ă©largir la fenĂȘtre rend l'incident improbable sans le rendre impossible. Et le seul signal qui a levĂ© le liĂšvre Ă©tait un job de scan rouge, symptĂŽme indirect : sans lui, le problĂšme serait apparu au prochain dĂ©marrage du cluster, sous la forme d'une prod qui ne dĂ©marre pas.


INC-063 : plus aucune métrique de l'API collectée depuis le cutover multi-env

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — correctif #152 mergĂ© (MR !268) et validĂ© live le 2026-07-28

Contexte : DĂ©couvert le 2026-07-28 en validant #135, par un contrĂŽle de non-rĂ©gression. Le correctif NetworkPolicy restreignait l'ingress de l'API Ă  Envoy et Prometheus ; il fallait donc vĂ©rifier que Prometheus scrapait toujours. La requĂȘte a renvoyĂ© zĂ©ro cible, ce qui a d'abord ressemblĂ© Ă  une rĂ©gression du correctif.

Fichiers : k8s/platform/monitoring/servicemonitor-fastapi.yaml, k8s/platform/monitoring/prometheusrules-fastapi.yaml

SymptÎme : /api/v1/targets ne renvoie aucune cible dont le namespace commence par fastapi. En parallÚle, l'alerte FastAPITargetMissing est en firing / critical depuis le démarrage du cluster.

Cause : Le ServiceMonitor portait namespaceSelector.matchNames: [fastapi]. Or le cutover multi-env (#134, 2026-07-25) a fait de fastapi un namespace plateforme qui n'hĂ©berge plus que le Gateway : il ne contient aucun Service. Les Services applicatifs vivent dĂ©sormais dans fastapi-dev, fastapi-staging et fastapi-prod, tous correctement labellisĂ©s app: fastapi. Le sĂ©lecteur pointait donc sur un namespace vide. Aggravant : les rĂšgles PrometheusRule sont Ă©pinglĂ©es sur le mĂȘme namespace="fastapi", donc FastAPITargetDown et FastAPIPodCrashLooping Ă©taient elles aussi inertes. Et les deux alertes RED n'avaient aucun filtre de namespace : rĂ©parer le seul scrape aurait fait paginer un pic d'erreurs de dev comme s'il venait de la prod.

Écart de diagnostic Ă  retenir : une NetworkPolicy qui bloque un scrape laisse la cible visible en down, avec un lastError. ZĂ©ro cible signifie que la dĂ©couverte de service n'a rien trouvĂ©, ce qui se situe en amont du rĂ©seau. C'est ce raisonnement qui a disculpĂ© #135 en une minute au lieu d'un revert inutile.

Fix : namespaceSelector: {any: true} sur le ServiceMonitor, et non une nouvelle liste de namespaces — reconduire une liste figĂ©e, c'est reprogrammer la mĂȘme panne au prochain changement de topologie. Le couple any: true + selector: app=fastapi suit les Services oĂč qu'ils aillent, et le label namespace reste portĂ© par chaque sĂ©rie. Les 5 rĂšgles d'alerte sont recentrĂ©es sur fastapi-prod : on page sur ce qui sert les utilisateurs. L'alerting par environnement relĂšve de #141.

Validation : Faite en live le 2026-07-28, juste aprĂšs le merge. Les 5 cibles apparaissent, une par pod, sur les trois environnements, toutes en up :

fastapi-dev     | up
fastapi-prod    | up
fastapi-prod    | up
fastapi-staging | up
fastapi-staging | up

FastAPITargetMissing a cessĂ© de sonner ; il ne reste que Watchdog et les deux alertes d'overcommit, attendues. Étape intermĂ©diaire instructive : Ă  la dĂ©couverte, les cibles sont apparues en unknown — elles existent mais n'ont pas encore Ă©tĂ© scrapĂ©es. C'est un troisiĂšme Ă©tat, distinct de up et de down, qu'il faut savoir attendre (intervalle 30s) avant de conclure Ă  un Ă©chec. RĂ©fĂ©rences rĂ©siduelles Ă  l'ancienne topologie nettoyĂ©es dans docs/validation-runbook.md et docs/comprendre/alerting.md.

Leçon : Un garde-fou peut fonctionner parfaitement et ne servir Ă  rien. FastAPITargetMissing a Ă©tĂ© Ă©crite exactement pour ce cas — son commentaire dit « absent() renvoie 1 quand la sĂ©rie est introuvable => couvre ce trou » — et elle a correctement sonnĂ© en critical pendant les trois jours d'uptime qui ont suivi le cutover. Ce qui a manquĂ©, c'est le regard sur l'alerte, pas l'alerte. Une chaĂźne de dĂ©tection ne vaut que par son dernier maillon, qui est humain. Corollaire de conception : un sĂ©lecteur qui nomme explicitement une ressource (matchNames, un namespace en dur dans une expr) crĂ©e une dĂ©pendance silencieuse Ă  la topologie — il ne casse pas quand elle change, il cesse de faire son travail. C'est la troisiĂšme occurrence du motif sur ce sprint aprĂšs SEC-006 et INC-062 : un changement d'architecture doit ĂȘtre suivi jusque dans les objets Ă©crits pour l'architecture prĂ©cĂ©dente, mĂȘme quand ils vivent dans une autre couche.


INC-064 : trivy-image-scan rouge sur deux CVE d'outillage jamais installé

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — correctif #155

Contexte : DĂ©couvert le 2026-07-30 en ouvrant !271 (#137), dont le pipeline est tombĂ© rouge alors que la MR ne touche ni Dockerfile, ni requirements.txt, ni aucun fichier Python. Le mĂȘme code applicatif passait au vert le 2026-07-28 Ă  18:45 (pipeline 2713010874, sha 332fcf1c), et l'image de base est pinnĂ©e par digest : elle n'avait pas bougĂ©. !271 a Ă©tĂ© mergĂ©e malgrĂ© ce rouge, l'Ă©chec Ă©tant prouvĂ© indĂ©pendant, ce qui a laissĂ© develop rouge sur ce job.

Fichiers : Dockerfile, .dockerignore

SymptÎme : trivy-image-scan échoue sur l'image backend avec deux vulnérabilités de sévérité haute : msgpack 1.1.2 (GHSA-6v7p-g79w-8964, corrigé en 1.2.1) et setuptools 70.3.0 (CVE-2025-47273, corrigé en 78.1.1). Ni l'un ni l'autre n'apparaßt dans requirements.txt, et la trace du job ne donne pas leur PkgPath.

Cause : Aucun des deux paquets n'est installĂ© dans l'image. Trivy les lit dans pip/_vendor/vendor.txt, le manifeste des dĂ©pendances vendorisĂ©es de pip, qu'il interprĂšte comme un inventaire de paquets installĂ©s — d'oĂč le PkgPath vide. L'image portait deux pip : celui de l'image de base (25.0.1, qui dĂ©clare msgpack==1.1.0) et celui du venv, montĂ© en 26.2 par la ligne pip install --no-cache-dir --upgrade pip wheel du stage builder puis recopiĂ© tel quel par COPY --from=builder /venv /venv (qui dĂ©clare msgpack==1.1.2). La mise Ă  jour de la base de vulnĂ©rabilitĂ©s de Trivy a fait franchir le seuil Ă  ces deux lignes : la CVE est arrivĂ©e au monde, pas au projet. Le commentaire du Dockerfile affirmait dĂ©jĂ  « ni pip, ni outils de build » — il dĂ©crivait une intention, pas l'image rĂ©ellement produite.

Écart de diagnostic Ă  retenir : le bump du digest de base, premiĂšre voie envisagĂ©e dans l'issue, n'aurait rien corrigĂ©. Le pip fautif est installĂ© par le Dockerfile lui-mĂȘme, pas hĂ©ritĂ© de la base, et le pip de la base vendorise une version de msgpack Ă©galement sous le seuil. Un PkgPath vide dans un rapport Trivy n'est pas un dĂ©tail cosmĂ©tique : il signale que le paquet n'a pas de dist-info, donc qu'il n'a jamais Ă©tĂ© installĂ©.

Fix : Suppression de pip et wheel du stage runtime (venv et site-packages de la base), plutĂŽt qu'une exception VEX. Rien n'installe de paquet Ă  l'exĂ©cution, la suppression est donc sans effet fonctionnel et retire la cause au lieu d'annoter le symptĂŽme — une exception VEX aurait dĂ» ĂȘtre réécrite Ă  chaque future CVE d'une dĂ©pendance vendorisĂ©e de pip. CorrigĂ© dans la foulĂ©e : frontend/ n'Ă©tait pas dans .dockerignore, donc la source du frontend partait dans l'image backend, et tout node_modules prĂ©sent dans le workspace de build y aurait injectĂ© ses propres CVE.

Validation : Faite en local sur l'image buildĂ©e depuis le Dockerfile corrigĂ©, avec les flags exacts du job CI (gate --exit-code 1 sur les sĂ©vĂ©ritĂ©s hautes et au-dessus, --ignore-unfixed, --ignorefile .trivyignore, --vex) : le gate sort en 0, contre deux vulnĂ©rabilitĂ©s hautes avant. Non-rĂ©gression applicative vĂ©rifiĂ©e sur la mĂȘme image : import app.main passe, uvicorn dĂ©marre, /healthz/live et /docs rĂ©pondent 200. Effet de bord mesurĂ© : 269 Mo → 234 Mo, l'essentiel venant de l'exclusion du frontend. Confirmation attendue par le trivy-image-scan de develop aprĂšs merge.

Leçon : Un scanner rapporte ce qu'il sait lire, pas ce qui est installĂ©. Trivy dĂ©duit un inventaire de fichiers manifestes, et vendor.txt en est un sans qu'aucun paquet ne soit dĂ©ployable derriĂšre. Prendre le rapport pour l'Ă©tat rĂ©el de l'image conduit Ă  traiter une remontĂ©e lĂ  oĂč elle est affichĂ©e plutĂŽt que lĂ  oĂč elle naĂźt — ici, Ă  bumper une image de base qui n'y Ă©tait pour rien. Le rĂ©flexe utile est de localiser physiquement le paquet dans l'image avant de choisir le traitement : c'est ce qui a fait apparaĂźtre qu'il n'y avait rien Ă  corriger, seulement de l'outillage Ă  ne pas embarquer. Second enseignement, sur le rouge lui-mĂȘme : un job de scan peut virer sans qu'une seule ligne du dĂ©pĂŽt ait changĂ©, puisque son verdict dĂ©pend d'une base de donnĂ©es externe et mouvante. Un pipeline rouge n'est donc pas toujours une rĂ©gression, et l'Ă©cart entre le dernier vert connu et le premier rouge est la premiĂšre mesure Ă  faire.


INC-065 : l'image de prod expirée d'ECR une seconde fois, la rétention élargie n'a fait que retarder l'échéance

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — contournĂ© le 2026-08-12, correctif structurel livrĂ© le 2026-08-15 (#150, MR !300 et !301) et validĂ© live le 2026-08-16

Contexte : DĂ©couvert le 2026-08-12 au montage du cluster, quinze jours aprĂšs la clĂŽture d'INC-062 et de #150. MĂȘme incident, mĂȘme cause, mĂȘme symptĂŽme, malgrĂ© le correctif validĂ© en live le 2026-07-28. Le cluster Ă©tait en cours de session, donc l'impact a Ă©tĂ© immĂ©diatement visible cette fois — contrairement Ă  INC-062, rĂ©vĂ©lĂ© par le rouge indirect d'un job de scan.

Fichiers : terraform/modules/ecr/main.tf, terraform/persistent/variables.tf, k8s/overlays/{fastapi,frontend}/prod/kustomization.yaml

SymptÎme : Les deux pods de fastapi-prod restent en Init:ImagePullBackOff, l'initContainer migrations échouant sur failed to resolve reference ... deployed-340a8e92...: not found. Les Applications ArgoCD fastapi-prod et frontend-prod restent Synced / Progressing indéfiniment : le manifeste est conforme au dépÎt, c'est son contenu qui ne résout plus. Les quatre autres envs sont Healthy, dev sur deployed-06875ca5 et staging sur deployed-f289f0a9.

Cause : Identique Ă  INC-062 : les trois environnements rĂ©fĂ©rencent des tags du mĂȘme moule deployed- et concourent donc dans la mĂȘme fenĂȘtre de rĂ©tention par count, alors que leurs cadences sont opposĂ©es. Le correctif de #150 a portĂ© deployed_retention_count de 5 Ă  30 sans changer cette propriĂ©tĂ© : il a seulement Ă©largi la fenĂȘtre. Prod Ă©tait figĂ©e sur deployed-340a8e92 depuis le 2026-07-28, sans promotion dĂ©libĂ©rĂ©e pendant que dev Ă©tait bumpĂ©e Ă  chaque merge sur develop. Quinze jours de merges ont suffi Ă  consommer les 30 places et Ă  Ă©jecter prod de nouveau. La leçon d'INC-062 l'avait Ă©crit mot pour mot : « Ă©largir la fenĂȘtre rend l'incident improbable sans le rendre impossible ».

Inventaire relevĂ© le 2026-08-12, qui confirme la cause et donne l'ordre de grandeur : 30 images portant un tag deployed- dans chacun des deux dĂ©pĂŽts, pour une rĂ©tention de 30, soit une fenĂȘtre pleine au ras du seuil, oĂč chaque poussĂ©e expire la plus ancienne. La politique appliquĂ©e cĂŽtĂ© AWS est conforme au dĂ©pĂŽt (p1 deployed- count 30, p2 candidate- Ăąge 3 j, p3 any count 5) : aucune dĂ©rive de configuration, le mĂ©canisme a fonctionnĂ© exactement comme Ă©crit.

PiĂšge de mesure Ă  connaĂźtre, il a failli innocenter la vraie cause. Une requĂȘte describe-images qui lit imageTags[0] sous-compte gravement, parce que chaque image porte deux tags sur le mĂȘme digest (candidate-<sha>-<pipeline> et deployed-<sha>, cf. le commentaire du module) et que l'ordre du tableau n'est pas garanti. La premiĂšre lecture donnait 14 images deployed- sur 30 places, ce qui semblait Ă©carter la rĂšgle de count et orienter le diagnostic vers une rĂšgle d'Ăąge inexistante. Il faut inspecter l'ensemble de imageTags.

Fix : Contournement immĂ©diat par une MR de promotion (ADR 029 D2) repointant les deux overlays de prod sur deployed-f289f0a9, le tag de staging, dont la prĂ©sence en registre est prouvĂ©e par les pods Healthy de fastapi-staging et frontend-staging dĂ©marrĂ©s Ă  la mĂȘme minute. Ce n'est pas un correctif, et la mesure dit Ă  quel point : deployed-f289f0a9 occupe la 14e place sur 30 dans les deux dĂ©pĂŽts, soit 13 images plus vieilles qu'elle. Comme le 2026-08-11 a produit Ă  lui seul 11 poussĂ©es, une seule journĂ©e soutenue la réévince. Le contournement achĂšte une journĂ©e, pas une semaine.

Second geste, un sursis assumĂ© : deployed_retention_count portĂ© de 30 Ă  100, avec une date de revue inscrite dans la variable (2026-09-30, ou la livraison de #150 si elle est antĂ©rieure). Le coĂ»t a Ă©tĂ© mesurĂ© pour Ă©carter l'objection budgĂ©taire : layers dĂ©dupliquĂ©s Ă  4,11 Go pour les deux dĂ©pĂŽts, soit 0,41 $/mois, avec une part marginale d'environ 86 Mo par image cĂŽtĂ© back et 6 Mo cĂŽtĂ© front — le passage Ă  100 ajoute donc de l'ordre de 0,60 $/mois. À ne pas confondre avec la somme brute des tailles d'images (6,93 Go), qui n'est pas ce qui est facturĂ©.

Le seul fix qui ferme le trou reste celui Ă©cartĂ© en #150 : sortir l'image servie de la fenĂȘtre de count au lieu d'espĂ©rer qu'elle y reste, via un tag posĂ© Ă  la promotion et protĂ©gĂ© par une rĂšgle prioritaire. Un prĂ©fixe inuse- unique ne suffirait pas : le write-back bumpant dev Ă  chaque merge, les tags de dev rempliraient la fenĂȘtre inuse- et prod y concourrait de nouveau, soit le mĂȘme bug dĂ©placĂ© d'un cran. Il faut un prĂ©fixe par environnement (inuse-prod-, inuse-staging-, inuse-dev-), chacun avec sa rĂšgle et son compteur, ce qui rend la cadence de dev structurellement incapable d'atteindre la fenĂȘtre de prod. Cela impose un job CI qui pose un tag ECR au merge de la MR de promotion, promote.sh ne parlant qu'Ă  Kustomize. #150 est rouverte pour le porter.

Validation du contournement (2026-08-12) : À confirmer aprĂšs sync ArgoCD de la MR de promotion : les deux pods de fastapi-prod et ceux de frontend-prod doivent passer Running, et kubectl get events -A --field-selector reason=Failed | grep -i pull ne rien renvoyer. Effet de bord attendu et voulu : prod quitte une image prĂ©-#156 pour une image qui lit son URL d'API au runtime. Le configMapGenerator de k8s/overlays/frontend/prod est prĂ©sent et pointe sur https://api.devopsyouss.com, soit exactement l'URL que l'ancienne image codait en dur — le comportement observable ne change donc pas, mais #156 devient actif sur les trois envs, ce qu'il n'Ă©tait que sur deux.

Validation du correctif structurel (2026-08-16), premier montage depuis sa livraison : les six dĂ©ploiements des trois environnements dĂ©marrent — dix pods Running, 1/1, zĂ©ro redĂ©marrage, aucun ImagePullBackOff — et les quinze Applications ArgoCD sont Synced / Healthy.

Les images rĂ©ellement servies, lues sur les Deployments, confirment que c'est bien le mĂ©canisme protĂ©gĂ© qui tient : fastapi-staging et fastapi-prod tournent tous deux sur deployed-f289f0a9, le tag du 2026-08-12, et les frontends de mĂȘme. C'est exactement l'image que la seule fenĂȘtre deployed- aurait laissĂ© expirer, et que les tags inuse-staging- et inuse-prod- rĂ©clament dĂ©sormais. Corroboration incidente : les pods de staging et de prod portent le mĂȘme hash de ReplicaSet (6b75d4f7d4), donc la mĂȘme dĂ©finition de pod, image comprise.

DerniĂšre case, acquise sans intervention : le registry-scan du schedule de 8h le 2026-08-16 (pipeline 2763518508) est passĂ© au vert avec registry-scan pour unique job — constatĂ© en listant les jobs, jamais en lisant le statut du pipeline.

Leçon : Un correctif qui dĂ©place un seuil sans changer la propriĂ©tĂ© qui produit la panne ne fait qu'en rĂ©gler la date. #150 a Ă©tĂ© validĂ©e sur le bon critĂšre (la rĂ©tention est bien passĂ©e Ă  30, vĂ©rifiĂ© cĂŽtĂ© AWS) et fermĂ©e sur cette preuve, alors que le critĂšre qui comptait Ă©tait ailleurs : rien ne distinguait toujours l'image en service des autres. La validation portait sur le paramĂštre modifiĂ©, pas sur le mode de dĂ©faillance. Corollaire pour les revues : quand la leçon d'un incident Ă©crit noir sur blanc que le fix retenu est un palliatif, cette phrase est une prĂ©diction datĂ©e, pas une prĂ©caution rhĂ©torique — elle mĂ©rite une issue de suivi ouverte au moment mĂȘme oĂč l'on ferme l'incident, sinon elle se vĂ©rifie sans que personne ne l'attende. Correctif structurel, livrĂ© le 2026-08-15 (#150) : les environnements dont le tag stagne reçoivent leur propre fenĂȘtre de rĂ©tention. Un job CI tag-inuse-images pose un tag inuse-<env>-<sha> sur le digest que l'environnement sert rĂ©ellement, et la lifecycle policy garde 3 images par prĂ©fixe. ECR attribuant chaque image Ă  la premiĂšre rĂšgle dont elle matche le filtre, l'image de prod est rĂ©clamĂ©e par la rĂšgle inuse-prod- et n'entre plus jamais en concurrence avec la cadence de dev : la propriĂ©tĂ© qui produisait la panne disparaĂźt, au lieu que son Ă©chĂ©ance soit repoussĂ©e. L'ordre des rĂšgles compte, prod avant staging — quand une mĂȘme image est servie par les deux, c'est la fenĂȘtre la plus stable qui la protĂšge. Le job Ă©choue si un overlay rĂ©fĂ©rence un tag qui ne rĂ©sout plus, ce qui est prĂ©cisĂ©ment le contrĂŽle rĂ©clamĂ© au paragraphe suivant.

Rectification du mĂȘme jour, sur ce correctif : le design annoncĂ© plus haut prĂ©voyait trois prĂ©fixes, dev compris. LivrĂ© tel quel le matin, il a Ă©tĂ© corrigĂ© l'aprĂšs-midi aprĂšs l'inventaire du registre. Une rĂšgle inuse- Ă©tant plus prioritaire que la rĂšgle deployed-, elle ne s'ajoute pas Ă  cette derniĂšre : elle la remplace pour les images qu'elle rĂ©clame. Une fenĂȘtre inuse-dev- de 3 places faisait donc passer les images de dev de 100 places Ă  3 — soit environ deux heures de durĂ©e de vie Ă  la cadence du 2026-08-11 (11 poussĂ©es dans la journĂ©e). ConsĂ©quence : une MR de promotion recopiant le tag d'une image de dev plus vieille que trois merges aurait pointĂ© sur une image dĂ©jĂ  supprimĂ©e, rejouant INC-062 dans l'autre sens. Dev est retirĂ© de la liste : son tag Ă©tant bumpĂ© Ă  chaque merge, son image est toujours la plus rĂ©cente et n'a jamais Ă©tĂ© menacĂ©e par une rĂšgle de count — c'est prĂ©cisĂ©ment pourquoi prod tombait et pas lui. Une protection ne se distribue pas par symĂ©trie : elle va aux objets qui portent le risque.

Second enseignement, sur la dĂ©tection : Synced / Progressing est un Ă©tat qu'ArgoCD peut tenir indĂ©finiment sans alerter, parce que du point de vue GitOps tout est conforme — l'Ă©cart n'est pas entre le dĂ©pĂŽt et le cluster, il est entre le dĂ©pĂŽt et le registre, que rien ne surveille. Les deux occurrences ont d'ailleurs Ă©tĂ© dĂ©couvertes par des symptĂŽmes indirects, un job de scan rouge la premiĂšre fois et un ImagePullBackOff au montage la seconde : un contrĂŽle qui vĂ©rifie simplement que le tag de chaque overlay rĂ©sout encore transformerait une panne de dĂ©marrage en alerte, et ne dĂ©pend d'aucun des correctifs ci-dessus.


INC-066 : sous charge, la readiness expire et Envoy sert des 503, le throttling CPU retire la capacité au pire moment

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — correctif #169 mergĂ© (MR !299) et validĂ© live sous charge le 2026-08-16

Contexte : DĂ©couvert le 2026-08-12 par accident, pendant le test de charge menĂ© pour #167 (valider l'autoscaling par le chemin de production plutĂŽt que par le ClusterIP). L'objet du test Ă©tait la trajectoire du HPA ; les 503 sont apparus dans la sortie de l'injecteur sans ĂȘtre cherchĂ©s. Deux correctifs de probes venaient d'ĂȘtre livrĂ©s le 2026-08-11 (#162, timeouts explicites) et la liveness, elle, tenait.

Fichiers : k8s/base/deployment.yaml, app/routers/health.py

SymptĂŽme : Des 503 servis aux clients pendant toute la fenĂȘtre de charge, par le chemin rĂ©el (NLB → Envoy → pods) :

{"level":"warn","msg":"Non ok http code","code":503,"status":"HTTP/1.1 503","thread":32}
{"level":"warn","msg":"Non ok http code","code":503,"status":"HTTP/1.1 503","thread":127}

En regard, la readiness en échec sur les quatre pods de fastapi-prod, dont deux tournaient depuis plus d'une heure :

Warning  Unhealthy  pod/
-dxtv2  Readiness probe failed: Get "http://10.42.1.227:8080/healthz/ready":
                                 context deadline exceeded (Client.Timeout exceeded while awaiting headers)

Zéro redémarrage sur les quatre pods. Ce détail isole la fautive : la liveness tient, donc le correctif de #162 fait bien ce qu'il visait, et l'ancienneté des pods écarte un problÚme de démarrage.

Cause : Le conteneur applicatif Ă©tait plafonnĂ© Ă  limits.cpu: 500m pour une requests.cpu: 100m. Au-delĂ  du plafond, Linux ne tue pas le processus, il le met en pause par intermittence — c'est le throttling CPU. MesurĂ©s sous charge, les pods collent au plafond : 496m et 491m.

Throttlés, ils ralentissent sur tout, y compris pour répondre à la readiness. Or cette sonde est coûteuse : /healthz/ready fait un SELECT 1 sur la base (app/routers/health.py:23). La réponse dépasse les 3 s accordées, trois échecs consécutifs suffisent (15 s), et le pod sort de l'EndpointSlice. Envoy perd un upstream, puis un autre, et répond 503 quand il n'en a plus.

La boucle est le vrai dĂ©faut : le trafic du pod retirĂ© se reporte sur les survivants, qui saturent davantage, dont les sondes Ă©chouent Ă  leur tour. Un pod est retirĂ© du service parce qu'il est occupĂ©, et le retirer rend les autres plus occupĂ©s encore. Le systĂšme se prive de capacitĂ© au moment prĂ©cis oĂč il en manque.

Cause de fond, en amont de la mĂ©canique : l'ADR 025 avait dĂ©crit ce mode de dĂ©faillance avant qu'il n'arrive. Sa DĂ©cision 2 prescrit une limite CPU « large ou absente sur le latency-sensitive (le throttling CPU fait souvent plus de mal qu'un lĂ©ger dĂ©passement) ». La configuration mettait 500m, soit 5× la request, sur le service le plus sensible Ă  la latence du cluster. La documentation prescrivait une chose et la configuration en faisait une autre, sans que rien ne relĂšve l'Ă©cart.

Fix : limits.cpu retirĂ©e du conteneur applicatif dans k8s/base/deployment.yaml, donc pour les trois environnements. Le CPU Ă©tant compressible, un dĂ©passement ralentit mais ne tue pas — l'inverse de la mĂ©moire, dont la limite est conservĂ©e. La piste « porter le timeoutSeconds de la readiness de 3 s Ă  5 s » a Ă©tĂ© Ă©cartĂ©e comme correctif principal : elle traite le symptĂŽme sans lever le throttling, et retarde d'autant la dĂ©tection d'une vraie panne de base. Aligner la configuration sur l'ADR existant a Ă©tĂ© prĂ©fĂ©rĂ© Ă  l'ajout d'un rĂ©glage compensatoire.

Trois vérifications faites avant, chacune sur une façon dont la limite aurait pu revenir par la bande ou bloquer le livrable :

  • le ResourceQuota de chaque overlay ne dĂ©clare pas limits.cpu — s'il le faisait, il forcerait chaque pod du namespace Ă  en porter une ;
  • le LimitRange default-limits posĂ© par le bootstrap Ansible n'a pas de default.cpu, seulement default.memory et des defaultRequest ;
  • le check kube-linter unset-cpu-requirements porte le paramĂštre requirementsType: request et n'exige aucune limite CPU. C'est l'arbitrage que la DĂ©cision 2 de l'ADR 025 laissait explicitement en suspens ; il est tranchĂ© par la mesure et non par l'hypothĂšse.

Validation : ComplÚte depuis le 2026-08-16. Vérifié hors cluster : kube-linter lint k8s/base/ k8s/frontend/ sort en 0 avec la version épinglée en CI (v0.8.3, identique en local), et kubectl kustomize k8s/overlays/fastapi/prod/ rend bien un conteneur applicatif sans limits.cpu, l'initContainer de migrations conservant la sienne (250m, non latency-sensitive).

VĂ©rifiĂ© en live sur fastapi-prod, charge par le chemin Envoy (150 connexions concurrentes, 300 s, 200 580 requĂȘtes Ă  667,8 req/s) : aucun context deadline exceeded dans les events du namespace, et l'EndpointSlice reste complet du dĂ©but Ă  la fin. Les seuls Readiness probe failed de la fenĂȘtre sont des connection refused au dĂ©marrage des pods ajoutĂ©s par le HPA, c'est-Ă -dire la course normale entre la sonde et le boot de l'application — l'inverse exact de la signature du 12/08, qui touchait des pods vieux de plus d'une heure.

Ce que la validation a rĂ©vĂ©lĂ©, et qui n'Ă©tait pas visible avant : 35 requĂȘtes sur 200 580 (0,017 %) reçoivent encore un 503. Elles portent le flag Envoy UC et le dĂ©tail upstream_reset_before_response_started{connection_termination}, non UH — donc l'upstream existait et Ă©tait sain, il a fermĂ© la connexion. Cause distincte, sans rapport avec le throttling : uvicorn ferme ses connexions inactives au bout de 5 s (dĂ©faut, aucun --timeout-keep-alive dans Dockerfile:67) quand Envoy les garde bien plus longtemps dans son pool. Suivi en #172. Un second dĂ©faut vivait donc derriĂšre le premier, et seule sa correction l'a rendu visible.

Mécanisme complet des deux pannes et méthode de lecture : Le 503 sous charge.

Leçon : Une dĂ©cision d'architecture Ă©crite ne se vĂ©rifie pas toute seule. L'ADR 025 nommait le mĂ©canisme, la consĂ©quence et le type de composant concernĂ© ; il ne manquait qu'un contrĂŽle reliant la prescription Ă  la valeur rĂ©ellement livrĂ©e. Entre les deux, la configuration a dĂ©rivĂ© sans bruit : la limite de 500m Ă©tait un reste de right-sizing (#112) oĂč seul le sur-dimensionnement Ă©tait en cause, et personne ne l'a relue Ă  la lumiĂšre de la DĂ©cision 2. Le corollaire pratique est que le passage d'une prescription Ă  un contrĂŽle automatisĂ© est le seul moment oĂč elle acquiert une force, sans quoi elle documente aprĂšs coup la panne au lieu de l'empĂȘcher.

Second enseignement, sur la dĂ©tection : ce dĂ©faut a Ă©tĂ© trouvĂ© pendant le test d'autre chose. Il ne remontait dans aucune alerte, parce que du point de vue de Kubernetes tout fonctionnait comme configurĂ© — les sondes Ă©chouaient, le pod Ă©tait retirĂ©, le mĂ©canisme faisait son travail. Un 503 servi au client est pourtant la seule mesure qui compte pour l'utilisateur, et c'est prĂ©cisĂ©ment celle qu'aucune sonde interne ne produit. C'est l'argument du monitoring boĂźte noire suivi en #141.


INC-067 : le keep-alive d'uvicorn plus court que le pool d'Envoy, un 503 sur 5 700 servi aux clients

SĂ©vĂ©ritĂ© : Medium Statut : ✅ RĂ©solu — correctif #172 mergĂ© (MR !309) et validĂ© live le 2026-08-16

Contexte : DĂ©couvert le 2026-08-16 en validant INC-066. Le correctif de #169 venait d'ĂȘtre confirmĂ© — plus aucune readiness en Ă©chec sous charge — mais des 503 subsistaient dans la sortie de l'injecteur. Ils Ă©taient invisibles tant que la premiĂšre panne durait : elle en produisait des milliers, et trente-cinq s'y noyaient.

Fichiers : k8s/base/backendtrafficpolicy-idle.yaml (créé), Dockerfile:67

SymptĂŽme : Sous charge par le chemin rĂ©el (Envoy → pods), 35 requĂȘtes sur 200 580 reçoivent un 503, soit une sur 5 731. RĂ©parties sur toute la fenĂȘtre, avec un groupement autour des scale-up du HPA.

Un second chiffre du mĂȘme rapport nomme dĂ©jĂ  la nature du dĂ©faut :

Code 200 : 200545 (100.0 %)
Code 503 : 35 (0.0 %)
Sockets used: 185 (for perfect keepalive, would be 150)

35 sockets en trop, exactement 35 erreurs. Chaque 503 a coûté sa connexion, ce qui n'arrive pas quand un serveur renvoie un code d'erreur sur une connexion saine.

Cause : Un désaccord de délai d'inactivité entre le proxy et l'application, deux réglages qui ne se consultent pas.

Dockerfile:67 lance uvicorn app.main:app --host 0.0.0.0 --port 8080, sans --timeout-keep-alive. Uvicorn applique donc son défaut, 5 secondes : passé ce délai sans trafic, il ferme la connexion. Envoy, lui, garde ses connexions vers le pod dans un pool avec un délai d'inactivité de l'ordre de l'heure, et n'est prévenu de rien.

Le 503 survient quand les deux gestes se croisent : Envoy Ă©met une requĂȘte sur la milliseconde oĂč uvicorn ferme la socket. Le log d'accĂšs le dit mot pour mot :

"response_code": 503,
"response_flags": "UC",
"response_code_details": "upstream_reset_before_response_started{connection_termination}"

UC = UpstreamConnectionTermination. À ne pas confondre avec UH (plus aucun upstream sain), qui Ă©tait le mĂ©canisme d'INC-066. Ici l'upstream existait et Ă©tait sain : il a raccrochĂ©.

La raretĂ© s'explique par la conjonction requise — une connexion doit rester inactive plus de 5 s et une requĂȘte doit arriver pile Ă  sa fermeture. Sous forte charge les connexions sont rarement inactives ; c'est au scale-up, quand l'arrivĂ©e de nouveaux pods redistribue le pool, que certaines se retrouvent sans travail.

Fix : Un BackendTrafficPolicy Envoy Gateway posant timeout.http.connectionIdleTimeout: 3s, dans k8s/base/ donc sur les trois environnements. La rĂšgle : le dĂ©lai d'inactivitĂ© du proxy doit ĂȘtre plus court que celui de l'application, pour que ce soit toujours le proxy qui ferme, au moment qu'il choisit. 3 s contre 5 s, deux secondes de marge.

La policy vise la HTTPRoute de l'environnement et non le Gateway partagé : targetRefs est un LocalPolicyTargetReference (lu sur la CRD du cluster), et viser le Gateway aurait couvert Grafana et ArgoCD, dont les upstreams n'ont rien à voir avec uvicorn.

Deux pistes écartées. Allonger --timeout-keep-alive cÎté uvicorn : il faudrait dépasser le délai d'Envoy, soit une heure, et immobiliser des connexions pour rien. Ajouter un retry sur reset : il masquerait le symptÎme au lieu de traiter la cause, et surtout changer deux choses à la fois aurait rendu le tir de validation ininterprétable. Gardée en réserve.

Validation : Rejeu de la charge aprĂšs sync ArgoCD, policy Accepted=True, 87 578 requĂȘtes en 120 s Ă  729,2 req/s :

Code 200 : 87578 (100.0 %)
Sockets used: 159 (for perfect keepalive, would be 150)

ZĂ©ro 503, lĂ  oĂč le rythme prĂ©cĂ©dent (35 pour 300 s) en aurait fait attendre une quinzaine sur 120 s. Le compte des sockets boucle la dĂ©monstration : les 9 en trop correspondent exactement aux 9 Closing dead socket de l'injecteur (4 puis 5), c'est-Ă -dire Ă  ses propres abandons cĂŽtĂ© client sous saturation — un phĂ©nomĂšne distinct, dĂ©jĂ  prĂ©sent avant le correctif. Plus une seule socket imputable Ă  la course de fermeture.

VĂ©rification directe dans Loki, {namespace="envoy-gateway-system", container="envoy"} |= "\"response_flags\":\"UC\"" : aucun UC sur la route fastapi. La seule occurrence de la fenĂȘtre porte sur grafana.devopsyouss.com, un WebSocket /api/live/ws ouvert 93 minutes, en upstream_reset_**after**_response_started — donc une fermeture normale aprĂšs rĂ©ponse commencĂ©e, l'inverse du cas traitĂ©.

Leçon : Deux dĂ©lais qui ne se parlent pas finissent par se croiser. DĂšs qu'un rĂ©glage de durĂ©e existe des deux cĂŽtĂ©s d'une connexion, les deux valeurs doivent ĂȘtre Ă©crites en regard, et il faut dĂ©cider quel bout ferme en premier. Ici le manifeste porte les deux : relever le --timeout-keep-alive d'uvicorn sans toucher aux 3 s serait sans effet, le baisser sous 3 s rouvrirait la panne.

Second enseignement, sur la validation : corriger une panne en a révélé une autre. La seconde n'a pas été créée par le correctif, elle était masquée par la premiÚre. Une validation qui se serait contentée de constater que le symptÎme avait fortement baissé aurait fermé le dossier sur un défaut toujours servi aux clients. C'est le response_flags d'Envoy qui a séparé les deux, et il était disponible dÚs le premier jour.

TroisiĂšme, sur l'outillage : ces lignes de log ne sont pas rĂ©cupĂ©rables par kubectl logs. À 667 requĂȘtes par seconde, Envoy produit environ 160 Mo de JSON en cinq minutes et kubelet fait tourner ses fichiers en une Ă  deux minutes. Deux tentatives ont Ă©chouĂ© avant qu'on pense Ă  Loki, qui reçoit les lignes d'Alloy avant la rotation et qui tourne depuis #75. kubectl logs est la fenĂȘtre vivante, Loki est la mĂ©moire. MĂ©canisme complet et mĂ©thode de lecture : Le 503 sous charge.


INC-068 : une CVE systÚme bloque tous les builds, et la base pinnée par digest ne se corrige pas par un rebuild

SĂ©vĂ©ritĂ© : High Statut : ✅ RĂ©solu — correctif #180

Contexte : Découvert le 2026-08-17 au matin, sur deux signaux qui semblaient n'en faire qu'un. Le schedule Vuln registry scan de 8h était rouge (pipeline 2764948654), et les trois merge requests Renovate ouvertes l'étaient aussi. Les deux causes étaient distinctes. Les MR n'avaient jamais démarré : failure_reason: stuck_pending_no_matching_runners, started_at: None, 6 865 s d'attente, le runner .112 étant éteint quand Renovate a poussé à 06:15. Ce n'est qu'aprÚs son rallumage et la relance de !317 que le vrai défaut est apparu, sur trivy-image-scan.

Fichiers : backend/Dockerfile

SymptĂŽme : trivy-image-scan Ă©choue sur l'image candidate avec Total: 9 (HIGH: 9, CRITICAL: 0), toutes portĂ©es par CVE-2026-53615 (integer overflow), corrigĂ©e par Debian en 2.41.5-0+deb13u1. Les 9 paquets remontĂ©s — bsdutils, libblkid1, liblastlog2-2, libmount1, libsmartcols1, libuuid1, login, mount, util-linux — sont tous issus du mĂȘme paquet source util-linux : une seule CVE, neuf lignes de rapport. Le job registry-scan du matin remontait exactement les mĂȘmes 9 sur l'image deployed-f289f0a9, et sortait en exit code 1 aprĂšs avoir postĂ© son alerte Slack, ce qui est son comportement attendu.

Cause : L'image de base est pinnĂ©e par digest (python:3.12-slim@sha256:57cd7c3a...), choix assumĂ© de #65 pour que l'image scannĂ©e soit exactement l'image dĂ©ployĂ©e. Un digest fige l'image bit pour bit, donc reconstruire reproduit la mĂȘme base et les mĂȘmes paquets vulnĂ©rables. Le correctif Debian existait dĂ©jĂ  en dĂ©pĂŽt, mais l'image amont n'avait pas encore Ă©tĂ© reconstruite pour l'intĂ©grer. Le Dockerfile portait dĂ©jĂ  un --only-upgrade de rattrapage, Ă©crit pour la CVE OpenSSL, qui ne couvrait que libssl3t64, openssl et openssl-provider-legacy.

Fix : Extension de ce mĂȘme --only-upgrade aux 9 paquets util-linux, avec un commentaire qui nomme les deux CVE traitĂ©es et la condition de retrait — quand le digest amont sera bumpĂ©. Le bump de digest par Renovate, seule alternative structurelle, vit dans !262, bloquĂ©e depuis le 2026-08-12 par deux problĂšmes sans rapport. L'attendre aurait laissĂ© tout build rouge pendant la fenĂȘtre d'audit du 18 au 31/08.

Validation : Faite en local sur l'image buildĂ©e depuis le Dockerfile corrigĂ©, avec les flags exacts du job CI (--exit-code 1 sur CRITICAL,HIGH, --ignore-unfixed, --ignorefile, --vex) : gate Ă  0, couche debian 13.6 Ă  0 vulnĂ©rabilitĂ© contre 9 avant. Preuve directe par dpkg-query sur l'image produite : util-linux et mount en 2.41.5-0+deb13u1, bsdutils en 1:2.41.5-0+deb13u1, contre 1:2.41-5 auparavant. Le critĂšre discrimine donc bien : rouge avant, vert aprĂšs, sur la mĂȘme commande.

Leçon : Épingler par digest fige aussi les failles. Le pin apporte une vraie garantie, l'image scannĂ©e est l'image dĂ©ployĂ©e, mais il transfĂšre au projet une charge que le tag latest portait implicitement : plus rien ne rĂ©cupĂšre les correctifs de sĂ©curitĂ© amont, et un rebuild, geste rĂ©flexe devant une CVE systĂšme, ne change rien. La contrepartie du pin est donc une voie de rattrapage explicite dans le Dockerfile, et un bump de digest volontaire quand l'amont a rattrapĂ©.

Second enseignement, sur le diagnostic : deux signaux rouges le mĂȘme matin n'ont pas forcĂ©ment la mĂȘme cause. Le rouge du schedule Ă©tait un vrai dĂ©faut, celui des MR Renovate n'Ă©tait qu'un runner Ă©teint. Lire failure_reason et started_at a sĂ©parĂ© les deux en une requĂȘte ; conclure depuis la couleur aurait fait chercher un problĂšme de code dans des MR qui n'avaient jamais exĂ©cutĂ© une seule ligne.

TroisiĂšme, sur le signal lui-mĂȘme : registry-scan Ă©choue volontairement quand il trouve des CVE corrigeables. Son rouge est le message, pas la panne. Un rouge quotidien auquel on s'habitue cesse pourtant d'ĂȘtre un signal, ce que le dĂ©lai de dĂ©couverte confirme ici.