C6 — Observabilité et sécuritéDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 3 · DevOps

Cours 6Observabilité et sécurité

Savoir ce que fait un système en production, être alerté sur ce qui compte, et intégrer la sécurité au flux plutôt qu'à la fin.

2 chapitres · 6 h de travail estimé

  1. 1. Observabilité3 h
  2. 2. DevSecOps3 h

Chapitre 1 · 3 h

Observabilité

Métriques, journaux et traces ; Prometheus et Grafana ; centralisation des journaux ; alertes utiles contre bruit d'alertes ; SLI, SLO et budget d'erreur.

Le chapitre 6 a construit un déploiement canari : router 5 % du trafic, mesurer, décider. Le chapitre 9 a montré Kubernetes retirer des pods du service et arrêter un déploiement.

Deux mécanismes remarquables, et tous deux inutilisables tels quels — parce qu'il manque la partie qui regarde. Un canari sans mesure expose 5 % des utilisateurs et attend qu'ils se plaignent ; un orchestrateur qui redémarre en boucle sans que rien ne le signale produit une panne silencieuse.

L'observabilité est cette partie manquante. Sa question n'est pas « le serveur répond-il ? » mais « que fait mon système, et pourquoi ? ».

Trois piliers, trois questions

PilierNatureRépond àCoût
Métriquesnombres agrégés dans le tempsque quelque chose va malfaible
Journauxévénements discrets, horodatésquoi exactementmoyen à élevé
Tracestrajet d'une requête entre services, dans la chaîneélevé

L'ordre n'est pas arbitraire : c'est celui du diagnostic. Une métrique alerte — le taux d'erreur est passé de 0,1 % à 4 %. Les journaux disent de quoi il s'agit — telle requête échoue avec tel message. Une trace dit où le temps est passé quand cinq services sont impliqués et qu'aucun ne semble fautif.

Les trois ne se remplacent pas. Beaucoup d'équipes n'ont que des journaux et cherchent une tendance en les lisant, ce qui ne marche pas : un journal répond mal à « combien » et « depuis quand ».

Métriques

Prometheus interroge périodiquement les applications, qui exposent leurs compteurs sur une adresse dédiée. Ce modèle par interrogation a une conséquence utile : l'absence de réponse est elle-même une information — un service qui ne répond plus est immédiatement visible.

Chaque métrique porte des étiquettes qui la découpent : par code HTTP, par point d'entrée, par version. C'est ce qui permet de comparer la version 1.4 et la version 1.5 pendant un canari, et donc de décider.

Quatre indicateurs suffisent à surveiller un service, et ils portent le nom de signaux dorés : la latence, le trafic, les erreurs, la saturation — c'est-à-dire à quel point les ressources sont pleines.

Un point de méthode sur la latence, souvent manqué : on ne surveille jamais une moyenne. Une latence moyenne de 200 ms peut cacher 95 % des requêtes à 50 ms et 5 % à plusieurs secondes. On regarde des centiles — le 95e, le 99e — parce que ce sont eux que les utilisateurs subissent, et parce qu'ils bougent bien avant la moyenne.

Journaux

Deux règles, et la première a une raison précise.

Les journaux quittent la machine. Une machine qui tombe emporte ses fichiers, et un conteneur détruit emporte les siens — le chapitre 3 l'a dit. Sur vingt exemplaires, chercher dans vingt endroits est impraticable. On les envoie donc vers un système central.

Les journaux sont structurés. Une ligne de texte libre se cherche mal ; un enregistrement avec des champs se filtre.

2026-03-14 10:22:31 ERROR échec de la commande 4471 pour l'utilisateur 88{"ts":"2026-03-14T10:22:31Z","niveau":"error","message":"échec de commande", "commande":4471,"utilisateur":88,"trace":"a3f9c1","service":"paiement"}

Le champ trace est ce qui relie les enregistrements d'une même requête à travers les services. Sans lui, un incident dans une architecture distribuée demande de recouper des horodatages à la main — c'est faisable sur deux services, impossible sur dix.

Deux mises en garde. Les journaux coûtent : en stockage, en indexation, en bande passante, et le niveau debug en production peut coûter plus cher que le service lui-même. Et ils contiennent des données personnelles — journaliser un corps de requête revient souvent à journaliser une adresse ou un moyen de paiement, ce qui a des conséquences réglementaires.

Alertes utiles, et bruit d'alertes

C'est la partie la plus importante du chapitre, et la plus mal faite en pratique.

Le mécanisme de dégradation est connu. On ajoute des alertes « au cas où ». Certaines se déclenchent sans qu'il y ait rien à faire. L'équipe apprend à les ignorer, met en sourdine celles qui reviennent le plus, et le jour où une vraie alerte part, elle est traitée comme les autres. C'est exactement le sort du test instable du chapitre 5.

Trois règles l'évitent.

Alerter sur les symptômes, pas sur les causes. « Le taux d'erreur dépasse 2 % » concerne les utilisateurs. « L'usage processeur dépasse 80 % » ne concerne personne : si le service répond correctement, il n'y a rien à faire. La cause s'investigue dans les tableaux de bord, une fois alerté par le symptôme.

Toute alerte doit être actionnable. La question à poser pour chaque alerte est : « que fait la personne réveillée ? » Sans réponse précise, l'alerte devient un tableau de bord, pas une alerte.

Distinguer ce qui réveille de ce qui attend. Une dégradation qui menace le service justifie une notification immédiate ; un disque à 70 % justifie un ticket. Mélanger les deux détruit la première catégorie.

SLI, SLO et budget d'erreur

Ces trois notions donnent au chapitre son cadre, et elles règlent un débat vieux comme le chapitre 1.

Un SLI est un indicateur de service : la part des requêtes servies en moins de 300 ms, par exemple. Un SLO est l'objectif qu'on se donne dessus : « 99,9 % des requêtes en moins de 300 ms, sur trente jours ».

Le budget d'erreur est le complément : 100%99,9%=0,1%100\,\% - 99{,}9\,\% = 0{,}1\,\%. Sur trente jours, cela représente 43 minutes d'indisponibilité autorisées.

Le renversement est là. Ce budget n'est pas une tolérance honteuse : c'est une ressource à dépenser. Tant qu'il reste du budget, l'équipe peut déployer vite et prendre des risques — le canari du chapitre 6, une refonte, une migration. Quand il est épuisé, on gèle les changements risqués et l'on consacre l'effort à la fiabilité.

Cette mécanique transforme une dispute en arithmétique. « Faut-il livrer vite ou être stable ? » n'est plus une question d'opinion entre deux équipes aux objectifs opposés : c'est un budget, mesuré, que tout le monde regarde. Et un SLO à 100 % est un mauvais SLO — il interdit tout changement, et il coûte un ordre de grandeur de plus que 99,9 % pour un bénéfice que personne ne perçoit.

Quiz · 1 question

Une équipe reçoit soixante alertes par jour, dont deux ou trois exigent une action. Quel est le vrai risque ?

  • Le coût du système de supervision, qui traite trop de donnéescoût technique
  • L'équipe apprend à ignorer les alertes : le jour où une vraie part, elle est traitée comme les autres. Le bruit ne fait pas perdre du temps, il DÉTRUIT LE SIGNALdestruction du signal
  • Aucun : mieux vaut trop d'alertes que pas assez, tant qu'elles sont archivéesaucun risque

Réponse : C'est exactement le mécanisme du test instable du chapitre 5, transposé à l'exploitation. Une alerte qui se déclenche sans qu'il y ait rien à faire enseigne à l'équipe que « rouge » ne veut rien dire ; on met en sourdine celles qui reviennent, on regarde les autres distraitement, et le jour où le taux d'erreur explose réellement, l'alerte est noyée dans les cinquante-sept autres. Trois règles l'évitent. ALERTER SUR LES SYMPTÔMES, pas sur les causes : « le taux d'erreur dépasse 2 % » concerne les utilisateurs, « le processeur est à 80 % » ne concerne personne si le service répond. TOUTE ALERTE DOIT ÊTRE ACTIONNABLE : que fait la personne réveillée ? Sans réponse précise, c'est un tableau de bord, pas une alerte. Et DISTINGUER ce qui réveille de ce qui attend un ticket — mélanger les deux détruit la première catégorie.

Quiz · 1 question

Un SLO fixe 99,9 % de disponibilité sur trente jours. L'équipe a consommé 10 minutes de son budget d'erreur. Que peut-elle en faire ?

  • Rien : tout incident est un échec, et il faut geler les déploiements jusqu'à la fin du moistout incident est un échec
  • Il reste 33 minutes sur les 43 autorisées : le budget est une RESSOURCE À DÉPENSER — tant qu'il en reste, on peut déployer vite et prendre des risques ; quand il est épuisé, on gèle les changements risquésune ressource à dépenser
  • Relever le SLO à 99,99 % pour se donner plus de margerelever le SLO

Réponse : Le budget d'erreur est le complément du SLO : 100 % − 99,9 % = 0,1 %, soit 43 minutes sur trente jours. Le renversement conceptuel est de le voir comme une RESSOURCE plutôt que comme une tolérance honteuse. Tant qu'il en reste, l'équipe est autorisée à déployer vite, à tenter une migration, à lancer un canari — les incidents qui en découlent sont budgétés. Quand il est épuisé, on gèle les changements risqués et l'on consacre l'effort à la fiabilité. Cette mécanique transforme la dispute du chapitre 1 — livrer vite ou être stable ? — en arithmétique que les deux camps regardent ensemble. Relever le SLO à 99,99 % ferait l'inverse : il ne resterait que 4 minutes par mois, ce qui interdit presque tout changement et coûte un ordre de grandeur de plus, pour un bénéfice qu'aucun utilisateur ne perçoit. Un SLO à 100 % est un mauvais SLO.

La séance qui compte : casser la production

Tout ce chapitre s'apprend en une séance, et elle mérite d'être décrite parce qu'elle transforme une liste d'outils en réflexe.

On déploie volontairement une version défaillante. On laisse l'alerte se déclencher — et l'on mesure combien de temps elle a mis. On diagnostique par les journaux et les tableaux de bord, sans regarder le code fautif. On exécute le retour arrière du chapitre 6. Et l'on écrit un post-mortem, avec les cinq parties du chapitre 1.

Ce que la séance apprend et qu'aucun cours ne transmet : le temps réel entre la panne et l'alerte, celui entre l'alerte et le diagnostic, la sensation de chercher dans les journaux sous pression, et le soulagement d'avoir un retour arrière qui a déjà été testé.

C'est aussi la seule façon de vérifier que le dispositif fonctionne. Une alerte jamais déclenchée, un retour arrière jamais exécuté et un tableau de bord jamais consulté en incident ne sont pas des garanties : ce sont des hypothèses.

À vous

L'exercice calcule ce qui se discute habituellement à l'estime.

D'abord le budget d'erreur : à partir d'un journal d'incidents, vous calculerez la disponibilité réalisée, le budget consommé et ce qu'il reste — puis vous verrez ce que change un SLO à 99,99 % au lieu de 99,9 %.

Ensuite la qualité des alertes. Vous disposez d'un mois d'alertes, chacune étiquetée « vraie » ou « fausse », et vous mesurerez la précision — quelle part des alertes méritait une action — et le rappel — quelle part des vrais incidents a été détectée. Vous ajusterez le seuil et vous constaterez qu'on ne peut pas améliorer les deux à la fois.

Enfin, la chronologie d'un incident : détection, diagnostic, rétablissement. Vous calculerez le temps de rétablissement du chapitre 1, et vous verrez lequel des trois segments dominait — ce qui dit sur quoi travailler.

Exercice de code

Calculez un budget d'erreur, réglez un seuil d'alerte, puis trouvez le segment qui domine un incident.

Point de départ

// ── 1. Budget d'erreur ────────────────────────────────────────────────────
const MOIS_MINUTES = 30 * 24 * 60;   // 43 200

const INCIDENTS = [
  { jour: 3,  minutes: 4,  cause: "déploiement défaillant, retour arrière" },
  { jour: 11, minutes: 2,  cause: "sonde mal réglée, redémarrages" },
  { jour: 18, minutes: 22, cause: "panne du fournisseur de base" },
  { jour: 26, minutes: 6,  cause: "migration bloquante" },
];

function budget(slo, incidents) {
  const autorise = 0;    // ← à écrire : (1 − slo) × minutes du mois
  const consomme = 0;    // ← à écrire
  return { autorise, consomme, restant: autorise - consomme,
           realise: 0 }; // ← disponibilité réellement atteinte
}

// ── 2. Qualité des alertes ────────────────────────────────────────────────
// Chaque alerte : la valeur mesurée, et s'il y avait VRAIMENT un problème.
const ALERTES = [
  { valeur: 2.4, vraiProbleme: true },  { valeur: 0.9, vraiProbleme: false },
  { valeur: 5.1, vraiProbleme: true },  { valeur: 1.2, vraiProbleme: false },
  { valeur: 3.8, vraiProbleme: true },  { valeur: 0.6, vraiProbleme: false },
  { valeur: 1.8, vraiProbleme: false }, { valeur: 4.2, vraiProbleme: true },
  { valeur: 1.1, vraiProbleme: true },  { valeur: 0.8, vraiProbleme: false },
  { valeur: 2.9, vraiProbleme: false }, { valeur: 6.0, vraiProbleme: true },
];

function qualite(seuil, alertes) {
  // ← à écrire : précision = part des alertes qui méritaient une action ;
  //   rappel = part des vrais problèmes détectés.
  return { declenchees: 0, precision: 0, rappel: 0 };
}

// ── 3. Chronologie d'un incident ──────────────────────────────────────────
const CHRONOLOGIE = {
  panne: "10:04",        // le déploiement défaillant part
  alerte: "10:31",       // quelqu'un est prévenu
  diagnostic: "10:44",   // la cause est comprise
  retabli: "10:46",      // le retour arrière est terminé
};

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez budget(). Que reste-t-il à 99,9 % ? Et à 99,99 % ?
// 2. Écrivez qualite() et faites varier le seuil de 0,5 à 5. Peut-on
//    améliorer précision ET rappel en même temps ?
// 3. Découpez la chronologie en trois segments. Lequel domine, et sur quoi
//    faut-il donc travailler en priorité ?

console.log(JSON.stringify(budget(0.999, INCIDENTS)));

Solution

const MOIS_MINUTES = 30 * 24 * 60;

const INCIDENTS = [
  { jour: 3,  minutes: 4,  cause: "déploiement défaillant, retour arrière" },
  { jour: 11, minutes: 2,  cause: "sonde mal réglée, redémarrages" },
  { jour: 18, minutes: 22, cause: "panne du fournisseur de base" },
  { jour: 26, minutes: 6,  cause: "migration bloquante" },
];

function budget(slo, incidents) {
  const autorise = (1 - slo) * MOIS_MINUTES;
  const consomme = incidents.reduce((s, i) => s + i.minutes, 0);
  return { autorise, consomme, restant: autorise - consomme,
           realise: 1 - consomme / MOIS_MINUTES };
}

console.log("— 1. budget d'erreur —");
for (const slo of [0.99, 0.999, 0.9999]) {
  const b = budget(slo, INCIDENTS);
  console.log("   SLO " + (slo * 100).toFixed(2).padStart(6) + " % : " +
    "budget " + b.autorise.toFixed(0).padStart(4) + " min | consommé " +
    b.consomme + " min | restant " + b.restant.toFixed(0).padStart(5) + " min" +
    (b.restant < 0 ? "   BUDGET ÉPUISÉ : gel des changements risqués" : "   on peut déployer"));
}
console.log("   disponibilité réellement atteinte : " +
  (budget(0.999, INCIDENTS).realise * 100).toFixed(3) + " %");
console.log("   À 99,9 % il reste " + budget(0.999, INCIDENTS).restant.toFixed(0) +
            " minutes : l'équipe est autorisée à prendre des risques.");
console.log("   À 99,99 % le budget est dépassé dès le premier incident : ce SLO");
console.log("   interdirait presque tout changement, pour un bénéfice qu'aucun");
console.log("   utilisateur ne perçoit. Un SLO trop élevé coûte plus qu'il ne rapporte.");

console.log("");
console.log("— 2. précision et rappel des alertes —");
const ALERTES = [
  { valeur: 2.4, vraiProbleme: true },  { valeur: 0.9, vraiProbleme: false },
  { valeur: 5.1, vraiProbleme: true },  { valeur: 1.2, vraiProbleme: false },
  { valeur: 3.8, vraiProbleme: true },  { valeur: 0.6, vraiProbleme: false },
  { valeur: 1.8, vraiProbleme: false }, { valeur: 4.2, vraiProbleme: true },
  { valeur: 1.1, vraiProbleme: true },  { valeur: 0.8, vraiProbleme: false },
  { valeur: 2.9, vraiProbleme: false }, { valeur: 6.0, vraiProbleme: true },
];

function qualite(seuil, alertes) {
  const declenchees = alertes.filter((a) => a.valeur >= seuil);
  const vraisPositifs = declenchees.filter((a) => a.vraiProbleme).length;
  const totalVrais = alertes.filter((a) => a.vraiProbleme).length;
  return {
    declenchees: declenchees.length,
    // Précision : parmi ce qui a sonné, quelle part méritait une action ?
    precision: declenchees.length ? vraisPositifs / declenchees.length : 1,
    // Rappel : parmi les vrais problèmes, quelle part a sonné ?
    rappel: vraisPositifs / totalVrais,
  };
}

console.log("   seuil | alertes | précision | rappel | lecture");
for (const seuil of [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0]) {
  const q = qualite(seuil, ALERTES);
  const lecture = q.precision < 0.6 ? "bruyant : on apprend à ignorer"
                : q.rappel < 0.6 ? "silencieux : des incidents passent"
                : "acceptable";
  console.log("   " + seuil.toFixed(1).padStart(5) + " | " + String(q.declenchees).padStart(7) +
    " | " + (q.precision * 100).toFixed(0).padStart(8) + " %" +
    " | " + (q.rappel * 100).toFixed(0).padStart(5) + " %" + " | " + lecture);
}
console.log("   Les deux ne s'améliorent pas ensemble : abaisser le seuil détecte");
console.log("   plus d'incidents ET plus de faux positifs. Un seuil trop bas produit");
console.log("   le bruit qui détruit le signal ; trop haut, il laisse passer. Le");
console.log("   réglage est une décision, pas un réglage par défaut.");

console.log("");
console.log("— 3. anatomie d'un incident —");
const minutes = (h) => { const [a, b] = h.split(":").map(Number); return a * 60 + b; };
const C = { panne: "10:04", alerte: "10:31", diagnostic: "10:44", retabli: "10:46" };
const segments = [
  { nom: "panne → alerte (détection)", d: minutes(C.alerte) - minutes(C.panne) },
  { nom: "alerte → diagnostic", d: minutes(C.diagnostic) - minutes(C.alerte) },
  { nom: "diagnostic → rétabli (action)", d: minutes(C.retabli) - minutes(C.diagnostic) },
];
const total = segments.reduce((s, x) => s + x.d, 0);
for (const s of segments) {
  console.log("   " + s.nom.padEnd(32) + String(s.d).padStart(3) + " min  " +
    "█".repeat(s.d) + "  " + Math.round((s.d / total) * 100) + " %");
}
console.log("   temps de rétablissement : " + total + " min");
const pire = segments.reduce((a, b) => (b.d > a.d ? b : a));
console.log("   Le segment dominant est « " + pire.nom + " » : c'est là qu'il faut");
console.log("   travailler. Améliorer le retour arrière — déjà à 2 minutes — ne");
console.log("   gagnerait presque rien ; abaisser le délai de DÉTECTION diviserait");
console.log("   le temps de rétablissement par deux. C'est exactement ce que le");
console.log("   canari du chapitre 6 attendait de ce chapitre.");

En travaux pratiques

Travaux pratiques 10 · 2 h

Casser la production

La séance qui transforme une liste d'outils en réflexe : instrumenter, déployer une version défaillante, laisser l'alerte partir, diagnostiquer, revenir en arrière, et écrire le compte rendu.

Avant de commencer

  • Le déploiement du TP 9, qui tourne
  • Prometheus et Grafana dans le cluster, ou en Compose à côté
  • Une charge continue sur l'application (une boucle curl suffit)

Énoncé

  1. Exposer les métriquesFaites exposer par l'application un compteur de requêtes étiqueté par code de réponse, et un histogramme de latence. Vérifiez que Prometheus les récupère. Indice : Un compteur par code permet de calculer un TAUX d'erreur ; un compteur global ne le permet pas.
  2. Construire le tableau de bord minimalAffichez les quatre signaux dorés : latence au 95e centile, trafic, taux d'erreur, saturation. Rien d'autre. Indice : Une moyenne de latence cache exactement ce qu'on cherche : surveillez des centiles.
  3. Fixer un SLO et calculer le budgetChoisissez un objectif de disponibilité sur trente jours, calculez le budget d'erreur correspondant en minutes, et affichez la part déjà consommée.
  4. Écrire une seule alerteUne alerte, sur un symptôme, avec un seuil et une durée. Écrivez à côté, en une phrase, ce que fait la personne réveillée — si vous ne savez pas l'écrire, l'alerte n'est pas la bonne.
  5. Casser la productionSans prévenir vos camarades, déployez une version qui échoue sur une part des requêtes. Notez l'heure exacte.
  6. DiagnostiquerL'équipe d'astreinte — vos camarades — travaille SANS regarder le code déployé. Ils partent de l'alerte, passent au tableau de bord, puis aux journaux. Notez l'heure de l'alerte et celle du diagnostic.
  7. Revenir en arrièreExécutez le retour arrière et notez l'heure de rétablissement. Vérifiez sur le tableau de bord que le taux d'erreur redescend.
  8. Écrire le post-mortemCinq parties, comme au TP 1. Calculez les trois segments — détection, diagnostic, action — et dites lequel domine votre temps de rétablissement. C'est celui-là qu'il faudra travailler.

C'est réussi quand

  • L'alerte est partie sans intervention humaine
  • Le diagnostic a été fait sans lire le code fautif
  • Vous avez trois durées chiffrées, et vous savez laquelle réduire en priorité
  • Le post-mortem ne nomme personne et ses actions ont une échéance

Correction

L'instrumentationsrc/metriques.js
const requetes = new client.Counter({
name: "http_requetes_total",
help: "requêtes servies",
labelNames: ["route", "code"],       // l'étiquette rend le taux calculable
});

const duree = new client.Histogram({
name: "http_duree_secondes",
help: "latence",
buckets: [0.05, 0.1, 0.3, 1, 3],
});

Sans l'étiquette « code », on connaît le nombre de requêtes mais pas la part qui échoue — et c'est précisément ce dont l'alerte a besoin. Un histogramme, lui, permet de calculer des centiles ; une simple moyenne ne le permet pas.

Budget d'erreur
SLO 99,9 % sur 30 jours
budget = 0,1 % × 43 200 min = 43,2 min

incident de ce TP : 42 min d'erreurs à 30 %
consommation ≈ 42 × 0,30 = 12,6 min  →  29 % du budget mensuel

Le budget se raisonne en minutes d'indisponibilité ÉQUIVALENTES, pondérées par la part du trafic touchée. Un incident qui consomme un tiers du budget mensuel n'interdit rien ; trois de suite gèlent les changements risqués. C'est ce qui transforme la dispute « vite ou stable » en arithmétique.

L'alerte, et sa fiche d'actionalertes.yml
- alert: TauxErreurEleve
expr: |
  sum(rate(http_requetes_total{code=~"5.."}[5m]))
    / sum(rate(http_requetes_total[5m])) > 0.02
for: 2m
annotations:
  resume: "taux d'erreur au-dessus de 2 % depuis 2 minutes"
  action: |
    1. tableau de bord : la hausse suit-elle un déploiement ?
    2. si oui : kubectl rollout undo deployment/tickets
    3. sinon : journaux du service, filtrer sur code 5xx

Trois choses en font une bonne alerte. Elle porte sur un SYMPTÔME que les utilisateurs subissent, pas sur une cause comme l'usage processeur. Elle a une DURÉE (for: 2m), qui évite de réveiller quelqu'un pour un pic de dix secondes. Et elle est ACTIONNABLE : l'annotation dit quoi faire. Une alerte dont on ne sait pas écrire l'action est un graphique, pas une alerte.

La chronologie type, et sa lecture
10:04  déploiement de la version fautive
10:31  alerte reçue          → détection   27 min
10:44  cause identifiée      → diagnostic  13 min
10:46  retour arrière fini   → action       2 min
                             total        42 min

La DÉTECTION domine largement, et c'est le cas le plus fréquent en TP comme en production. Améliorer le retour arrière — déjà à deux minutes — ne gagnerait rien ; abaisser le seuil et la durée de l'alerte, ou surveiller pendant le déploiement, diviserait le total par deux. C'est aussi ce qui manquait au canari du TP 6 pour décider tout seul.

Ce que la séance apprend, et qu'aucun cours ne transmet

Le temps réel entre la panne et l'alerte. La sensation de chercher dans des journaux sous pression. Et le soulagement d'exécuter un retour arrière qui a DÉJÀ été testé. C'est aussi la seule façon de vérifier que le dispositif fonctionne : une alerte jamais déclenchée, un retour arrière jamais exécuté et un tableau de bord jamais consulté en incident ne sont pas des garanties, ce sont des hypothèses.

Ce que la suite en fait

Le chapitre 11 clôt le semestre en ajoutant la sécurité au flux plutôt qu'à sa fin. La logique sera la même que dans ce chapitre : mesurer, alerter sur ce qui compte, et éviter le bruit — un analyseur de vulnérabilités qui signale trois cents alertes non exploitables sera désactivé exactement comme une alerte trop bavarde.

À retenir

Flashcards · 5 cartes

Quels sont les trois piliers, et à quelle question répond chacun ?
MÉTRIQUES : nombres agrégés, faible coût, disent QUE quelque chose va mal — elles alertent. JOURNAUX : événements discrets horodatés, disent QUOI exactement. TRACES : trajet d'une requête entre services, disent OÙ dans la chaîne. L'ordre est celui du diagnostic, et les trois ne se remplacent pas : chercher une tendance en lisant des journaux ne marche pas, un journal répond mal à « combien » et « depuis quand ».
Quels sont les quatre signaux dorés, et pourquoi ne surveille-t-on pas une moyenne de latence ?
LATENCE, TRAFIC, ERREURS, SATURATION. Une latence MOYENNE de 200 ms peut cacher 95 % des requêtes à 50 ms et 5 % à plusieurs secondes : on surveille des CENTILES — 95e, 99e — parce que ce sont eux que les utilisateurs subissent, et parce qu'ils bougent bien avant la moyenne. À noter aussi : le modèle par interrogation de Prometheus rend l'ABSENCE de réponse informative.
Quelles sont les deux règles sur les journaux, et les deux mises en garde ?
1) ILS QUITTENT LA MACHINE : une machine qui tombe emporte ses fichiers, un conteneur détruit aussi, et chercher dans vingt endroits est impraticable. 2) ILS SONT STRUCTURÉS : des champs se filtrent, une ligne libre se cherche mal — et un identifiant de trace relie les enregistrements d'une même requête à travers les services. MISES EN GARDE : ils COÛTENT (stockage, indexation, bande passante — le niveau debug en production peut coûter plus que le service), et ils contiennent des DONNÉES PERSONNELLES.
Quelles sont les trois règles d'une bonne alerte ?
1) ALERTER SUR LES SYMPTÔMES, pas les causes : « le taux d'erreur dépasse 2 % » concerne les utilisateurs, « le processeur est à 80 % » ne concerne personne si le service répond. 2) TOUTE ALERTE DOIT ÊTRE ACTIONNABLE : que fait la personne réveillée ? Sans réponse précise, c'est un tableau de bord. 3) DISTINGUER ce qui réveille de ce qui attend un ticket. Sinon l'équipe apprend à ignorer les alertes, et le bruit ne fait pas perdre du temps : il DÉTRUIT LE SIGNAL.
Qu'est-ce qu'un budget d'erreur, et quel débat règle-t-il ?
Le complément du SLO : 100 % − 99,9 % = 0,1 %, soit 43 minutes sur trente jours. C'est une RESSOURCE À DÉPENSER, pas une tolérance honteuse : tant qu'il en reste, on déploie vite et l'on prend des risques ; épuisé, on gèle les changements risqués pour travailler la fiabilité. Il transforme la dispute « vite ou stable ? » en arithmétique que les deux camps regardent ensemble. Et un SLO à 100 % est un mauvais SLO : il interdit tout changement pour un bénéfice imperceptible.

Chapitre 2 · 3 h

DevSecOps

Décaler la sécurité vers la gauche, analyse des dépendances et des images, secrets jamais commités, moindre privilège, conformité intégrée au pipeline.

Dans le modèle traditionnel, la sécurité arrive à la fin : le développement livre, une équipe spécialisée audite, et rend un rapport de quatre-vingts pages deux semaines avant la mise en production.

Le résultat est prévisible, et c'est exactement le mur du chapitre 1 sous une autre forme. Les corrections coûtent cher parce qu'elles arrivent tard. Le rapport est partiellement ignoré parce qu'il n'est pas tenable dans le délai. Et l'équipe apprend que la sécurité est un obstacle plutôt qu'une exigence.

Décaler la sécurité vers la gauche consiste à la répartir tout au long du flux qu'on a construit — dans l'éditeur, dans la demande de fusion, dans le pipeline, dans l'exécution — plutôt qu'à la concentrer en une porte finale.

Le coût de la correction

L'argument est économique avant d'être moral.

DécouverteCoût relatifPourquoi
À l'écriture1le contexte est présent, rien n'est déployé
En revue5il faut réexpliquer, refaire passer le pipeline
En recette15l'artefact est à reconstruire, la validation à refaire
En production60incident, correctif d'urgence, parfois notification légale
Après exploitation par un tiers500+fuite de données, obligations réglementaires, réputation

Les ordres de grandeur varient selon les études ; la forme de la courbe, elle, ne varie pas. C'est la boucle de rétroaction du chapitre 1, appliquée à la sécurité : plus la détection est proche de l'écriture, moins elle coûte.

D'où le principe d'organisation : chaque étape du pipeline porte une vérification de sécurité, aucune n'attend la fin.

Les dépendances

C'est le sujet le plus important en volume, et le plus mal traité.

Une application moderne écrit quelques milliers de lignes et en importe des centaines de milliers. Un projet Node ou Python typique dépend de plusieurs centaines de paquets, dont la plupart n'ont jamais été choisis par personne : ce sont des dépendances transitives, tirées par d'autres dépendances.

L'analyse de composition compare cet arbre à des bases de vulnérabilités publiques et signale les paquets concernés. C'est indispensable, et cela produit immédiatement un problème que le chapitre 10 a déjà nommé : le bruit.

Un projet de taille moyenne remonte couramment cent à trois cents alertes. La quasi-totalité n'est pas exploitable dans son contexte : la vulnérabilité est dans une fonction jamais appelée, dans un outil de développement absent de l'image finale, ou demande un accès que l'attaquant n'a pas. Une équipe qui reçoit trois cents alertes ne les traite pas — elle les ignore, et une alerte vraiment grave se perd dans le tas.

Le tri repose donc sur trois questions, dans cet ordre, et pas sur le score de gravité seul.

Le code vulnérable est-il atteignable ? Une faille dans une fonction que le projet n'appelle jamais n'est pas exploitable. Les outils modernes savent en partie le déterminer, et c'est le filtre le plus efficace.

Est-elle dans l'image de production ? Une vulnérabilité dans une dépendance de développement, absente de l'image finale grâce à la construction multi-étapes du chapitre 3, ne menace rien.

Est-elle accessible depuis l'extérieur ? Une faille exploitable seulement avec un accès local sur une machine déjà compromise n'a pas la même urgence qu'une exécution de code à distance sur le point d'entrée public.

Deux mesures complètent l'analyse. Le verrouillage des versions — le fichier de verrouillage committé — garantit que tout le monde installe exactement les mêmes versions, et fait de la mise à jour une décision plutôt qu'un effet de bord. Et la mise à jour régulière en petits lots : c'est encore le chapitre 1, une montée de version par semaine étant infiniment moins risquée qu'une migration de deux ans de retard un jour d'urgence.

Les images

Une image hérite des vulnérabilités de sa base. Une image construite sur une distribution complète embarque des centaines de paquets dont l'application n'utilise aucun — et chacun est de la surface d'attaque.

Trois mesures, qui sont celles du chapitre 3 vues sous un autre angle.

Réduire la surface. Une image alpine ou distroless contient un ordre de grandeur de paquets en moins, donc un ordre de grandeur de vulnérabilités en moins — sans aucun effort de correction.

Ne pas tourner en root. Une faille d'application donne alors les droits d'un compte restreint, et non ceux de l'administrateur du conteneur.

Analyser l'image dans le pipeline, et pas seulement les dépendances applicatives : la base vieillit toute seule, sans qu'aucune ligne de code n'ait changé.

Les secrets

Le chapitre 8 a posé la règle et sa conséquence : un secret ne va jamais dans Git, et un secret commité doit être considéré comme compromis et changé, la réécriture d'historique n'étant qu'un nettoyage tardif.

Il reste à l'empêcher d'entrer, et cela se fait en trois couches complémentaires.

Avant le commit, un crochet local refuse un ajout contenant un motif de secret. Rapide, mais contournable et dépendant de la configuration de chaque poste.

Dans le pipeline, une analyse systématique de la demande de fusion. Non contournable — c'est la couche qui compte — mais elle intervient après que le secret est parti sur le serveur, donc la rotation reste nécessaire.

Dans l'exécution, les secrets ne sont jamais dans l'image mais injectés au démarrage, et le chapitre 3 rappelait que la variable d'environnement n'est pas un mécanisme de secret. Les coffres-forts du chapitre 8 ajoutent ce qui manque : journalisation des accès, rotation, et surtout durée de vie limitée — un identifiant valable une heure a beaucoup moins de valeur pour un attaquant qu'un mot de passe permanent.

Accès et moindre privilège

Le principe de moindre privilège est le même qu'au chapitre 8 du cours de systèmes : chaque acteur ne reçoit que les droits strictement nécessaires à sa tâche.

Trois applications concrètes dans le contexte de ce cours.

Le pipeline. C'est souvent l'acteur le plus privilégié de toute l'organisation — il peut déployer en production, lire les secrets, modifier l'infrastructure. Un dépôt compromis donne alors tout. On restreint donc par branche et par environnement, et l'on préfère des identifiants éphémères obtenus à l'exécution à des jetons permanents stockés dans l'outil.

Les comptes de service. Un service qui lit une base n'a pas besoin des droits d'écriture, ni de la table des utilisateurs. Le compte par défaut, souvent tout-puissant, est le raccourci à éviter.

Les accès humains. Nominatifs, révocables, et journalisés — c'est la traçabilité du chapitre 1 du cours de cybersécurité, sans laquelle aucun incident n'est analysable.

Conformité intégrée

Dernière idée du chapitre, et elle est cohérente avec tout le reste : une exigence de sécurité écrite dans un document est une intention ; écrite dans le pipeline, c'est une garantie.

Une politique comme code exprime des règles vérifiables automatiquement : aucun conteneur en root, aucun port de base exposé publiquement, chiffrement obligatoire sur les volumes, image provenant d'un registre approuvé. Le plan Terraform du chapitre 7 ou le manifeste Kubernetes du chapitre 9 sont validés contre ces règles avant application.

L'audit devient alors un sous-produit : plutôt que de reconstituer six mois après qui a déployé quoi, on dispose de l'historique Git, des exécutions de pipeline et des journaux d'accès. C'est la même bascule que dans tout le semestre — d'un contrôle ponctuel et manuel vers une propriété continue et automatique.

Quiz · 1 question

Un analyseur remonte 280 vulnérabilités sur un projet. Comment traiter cela ?

  • Les corriger toutes avant la prochaine mise en production, par ordre de gravité décroissantetout corriger
  • Trier par EXPLOITABILITÉ : le code vulnérable est-il atteignable ? est-il dans l'image de production ? est-il accessible de l'extérieur ? Sans ce tri, l'équipe ignore les 280 et perd aussi les gravestrier par exploitabilité
  • Désactiver l'analyseur : à ce volume, il n'apporte aucune informationdésactiver

Réponse : Deux cent quatre-vingts alertes sont ingérables, et c'est précisément le bruit du chapitre 10 : une équipe qui reçoit ce volume ne le traite pas, elle l'ignore — et une alerte vraiment grave se perd dans le tas. Corriger par ordre de gravité ne marche pas non plus, parce que le score de gravité est calculé DANS L'ABSOLU, sans connaître votre contexte : une note de 9,8 sur une fonction que votre code n'appelle jamais est moins urgente qu'une note de 6,5 sur votre point d'entrée public. Le tri se fait donc sur l'EXPLOITABILITÉ, en trois questions : le code vulnérable est-il atteignable depuis le vôtre ? est-il présent dans l'image de production, ou seulement dans une dépendance de développement écartée par la construction multi-étapes ? est-il accessible de l'extérieur ? Ce tri ramène couramment 280 alertes à cinq ou six qui méritent une action cette semaine.

Quiz · 1 question

Pourquoi le pipeline d'intégration continue est-il une cible privilégiée ?

  • Parce qu'il exécute du code non vérifié provenant de l'extérieurcode non vérifié
  • Parce qu'il est souvent l'acteur le PLUS PRIVILÉGIÉ de l'organisation : il déploie en production, lit les secrets et modifie l'infrastructure — compromettre le dépôt donne alors tout, sans avoir à attaquer la production directementconcentration de privilèges
  • Parce qu'il est généralement exposé sur Internet sans authentificationexposition réseau

Réponse : Le pipeline concentre des pouvoirs que personne ne détient individuellement : il peut construire, signer, déployer en production, lire les secrets de tous les environnements et modifier l'infrastructure décrite au chapitre 7. Un attaquant qui obtient la capacité d'y faire exécuter du code — par une dépendance compromise, une contribution malveillante, ou un jeton fuité — n'a plus besoin d'attaquer la production : il la déploie. C'est le principe de moindre privilège du cours de systèmes qui s'applique ici, et il se décline en trois mesures : restreindre les droits PAR BRANCHE et PAR ENVIRONNEMENT — une demande de fusion n'a aucune raison de pouvoir déployer en production —, préférer des identifiants ÉPHÉMÈRES obtenus à l'exécution à des jetons permanents stockés dans l'outil, et journaliser. L'exécution de code extérieur est un vecteur réel, mais c'est la concentration de privilèges qui en fait une cible.

À vous

L'exercice fait le tri que le chapitre décrit, et il montre ce que le tri change.

Vous disposez d'un arbre de dépendances, de vulnérabilités avec leur score, et de trois informations de contexte : la fonction vulnérable est-elle appelée, la dépendance est-elle dans l'image de production, la faille est-elle exploitable à distance. Vous écrirez le tri et comparerez le classement par gravité seule au classement par exploitabilité — les deux premières places ne sont pas les mêmes.

La seconde partie compare deux images de base sur le nombre de paquets et de vulnérabilités héritées, ce qui chiffre l'argument du chapitre 3.

La troisième audite des permissions : un compte de service, ses droits accordés et ses droits réellement utilisés sur un mois de journaux. Vous calculerez l'écart, qui est exactement ce que le moindre privilège demande de supprimer.

Exercice de code

Triez des vulnérabilités par exploitabilité, chiffrez l'image minimale, puis auditez des permissions.

Point de départ

// ── 1. Des vulnérabilités, et leur contexte ───────────────────────────────
const VULNERABILITES = [
  { id: "CVE-2025-1001", paquet: "lodash",       gravite: 9.8,
    atteignable: false, enProduction: true,  distance: true,
    note: "prototype pollution dans une fonction que le projet n'appelle pas" },
  { id: "CVE-2025-1002", paquet: "express",      gravite: 6.5,
    atteignable: true,  enProduction: true,  distance: true,
    note: "sur le point d'entrée public" },
  { id: "CVE-2025-1003", paquet: "webpack",      gravite: 8.1,
    atteignable: true,  enProduction: false, distance: false,
    note: "outil de construction, absent de l'image finale" },
  { id: "CVE-2025-1004", paquet: "postgres",     gravite: 7.5,
    atteignable: true,  enProduction: true,  distance: false,
    note: "exploitable avec un accès local sur la machine" },
  { id: "CVE-2025-1005", paquet: "jsonwebtoken", gravite: 7.2,
    atteignable: true,  enProduction: true,  distance: true,
    note: "vérification de signature contournable" },
  { id: "CVE-2025-1006", paquet: "chalk",        gravite: 5.3,
    atteignable: false, enProduction: false, distance: false,
    note: "coloration de sortie, développement seulement" },
];

function score(v) {
  // ← à écrire : pondérer la gravité par l'exploitabilité RÉELLE.
  //   Non atteignable, hors production, ou non exploitable à distance
  //   doivent faire chuter la priorité.
  return v.gravite;
}

// ── 2. Deux images de base ────────────────────────────────────────────────
const IMAGES = {
  "node:20":          { paquets: 412, vulnHautes: 17, vulnMoyennes: 63, mo: 1100 },
  "node:20-alpine":   { paquets: 47,  vulnHautes: 1,  vulnMoyennes: 4,  mo: 130 },
  "distroless/nodejs": { paquets: 12, vulnHautes: 0,  vulnMoyennes: 1,  mo: 110 },
};

// ── 3. Audit de permissions ───────────────────────────────────────────────
const COMPTE = {
  nom: "service-commandes",
  accordes: ["db:read", "db:write", "db:drop", "s3:read", "s3:write",
             "secrets:read", "users:read", "users:write", "deploy:prod"],
};
const JOURNAL_UN_MOIS = [
  "db:read", "db:read", "db:write", "db:read", "s3:read",
  "db:write", "db:read", "s3:read", "db:read", "db:write",
];

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez score(). Comparez le classement par GRAVITÉ et par
//    EXPLOITABILITÉ : les deux premières places sont-elles les mêmes ?
// 2. Chiffrez ce que l'image minimale supprime — sans aucune correction.
// 3. Calculez l'écart entre droits accordés et droits utilisés.

for (const v of VULNERABILITES) console.log("   " + v.id + "  " + v.gravite);

Solution

const VULNERABILITES = [
  { id: "CVE-2025-1001", paquet: "lodash", gravite: 9.8,
    atteignable: false, enProduction: true, distance: true,
    note: "fonction jamais appelée par le projet" },
  { id: "CVE-2025-1002", paquet: "express", gravite: 6.5,
    atteignable: true, enProduction: true, distance: true,
    note: "sur le point d'entrée public" },
  { id: "CVE-2025-1003", paquet: "webpack", gravite: 8.1,
    atteignable: true, enProduction: false, distance: false,
    note: "outil de construction, hors image finale" },
  { id: "CVE-2025-1004", paquet: "postgres", gravite: 7.5,
    atteignable: true, enProduction: true, distance: false,
    note: "demande un accès local" },
  { id: "CVE-2025-1005", paquet: "jsonwebtoken", gravite: 7.2,
    atteignable: true, enProduction: true, distance: true,
    note: "signature contournable" },
  { id: "CVE-2025-1006", paquet: "chalk", gravite: 5.3,
    atteignable: false, enProduction: false, distance: false,
    note: "développement seulement" },
];

function score(v) {
  // Le score de gravité est calculé DANS L'ABSOLU. On le pondère par le
  // contexte, dans l'ordre d'efficacité des trois filtres du cours.
  let s = v.gravite;
  if (!v.atteignable) s *= 0.15;    // le filtre le plus efficace
  if (!v.enProduction) s *= 0.10;   // écartée par la construction multi-étapes
  if (!v.distance) s *= 0.40;       // demande un accès déjà obtenu
  return s;
}

const classer = (cle) => [...VULNERABILITES].sort((a, b) => cle(b) - cle(a));

console.log("— 1. deux classements, deux priorités —");
console.log("   par GRAVITÉ seule                    | par EXPLOITABILITÉ");
const g = classer((v) => v.gravite), e = classer(score);
for (let i = 0; i < VULNERABILITES.length; i++) {
  console.log("   " + (i + 1) + ". " + g[i].paquet.padEnd(13) + g[i].gravite.toFixed(1) +
    "                | " + (i + 1) + ". " + e[i].paquet.padEnd(13) +
    score(e[i]).toFixed(2).padStart(5) + "  " + e[i].note.slice(0, 34));
}
console.log("   Les deux premières places diffèrent : lodash, noté 9,8, tombe en");
console.log("   queue parce que la fonction vulnérable n'est jamais appelée ; express,");
console.log("   noté 6,5, passe en tête parce qu'il est sur le point d'entrée public.");
const aTraiter = VULNERABILITES.filter((v) => score(v) >= 5);
console.log("   À traiter cette semaine : " + aTraiter.length + " sur " +
            VULNERABILITES.length + " — un volume tenable, donc réellement traité.");

console.log("");
console.log("— 2. ce que l'image minimale supprime, sans corriger une ligne —");
const IMAGES = {
  "node:20":           { paquets: 412, vulnHautes: 17, vulnMoyennes: 63, mo: 1100 },
  "node:20-alpine":    { paquets: 47,  vulnHautes: 1,  vulnMoyennes: 4,  mo: 130 },
  "distroless/nodejs": { paquets: 12,  vulnHautes: 0,  vulnMoyennes: 1,  mo: 110 },
};
const base = IMAGES["node:20"];
for (const [nom, i] of Object.entries(IMAGES)) {
  console.log("   " + nom.padEnd(20) + String(i.paquets).padStart(4) + " paquets | " +
    String(i.vulnHautes).padStart(2) + " hautes, " + String(i.vulnMoyennes).padStart(2) + " moyennes | " +
    String(i.mo).padStart(5) + " Mo" +
    (nom === "node:20" ? "" : "   −" + Math.round((1 - i.vulnHautes / base.vulnHautes) * 100) +
     " % de vulnérabilités hautes"));
}
console.log("   Changer une ligne de FROM supprime seize vulnérabilités hautes.");
console.log("   Aucune correction de code, aucune montée de version applicative :");
console.log("   c'est le meilleur rapport effort/résultat de tout le chapitre.");

console.log("");
console.log("— 3. audit de permissions —");
const COMPTE = { nom: "service-commandes",
  accordes: ["db:read", "db:write", "db:drop", "s3:read", "s3:write",
             "secrets:read", "users:read", "users:write", "deploy:prod"] };
const JOURNAL = ["db:read", "db:read", "db:write", "db:read", "s3:read",
                 "db:write", "db:read", "s3:read", "db:read", "db:write"];
const utilises = new Set(JOURNAL);
const inutiles = COMPTE.accordes.filter((d) => !utilises.has(d));
console.log("   compte : " + COMPTE.nom);
console.log("   accordés : " + COMPTE.accordes.length + " droits");
console.log("   utilisés : " + utilises.size + " droits — " + [...utilises].join(", "));
console.log("   JAMAIS UTILISÉS (" + inutiles.length + ") : " + inutiles.join(", "));
const graves = inutiles.filter((d) => /drop|secrets|deploy|users:write/.test(d));
console.log("   dont " + graves.length + " particulièrement dangereux : " + graves.join(", "));
console.log("   Écart : " + Math.round((inutiles.length / COMPTE.accordes.length) * 100) +
            " % des droits accordés ne servent à rien.");
console.log("   Un service qui lit des commandes peut détruire des tables, lire tous");
console.log("   les secrets et déployer en production. Le moindre privilège consiste");
console.log("   exactement à supprimer cet écart — et un mois de journaux suffit à");
console.log("   le calculer, sans deviner.");

En travaux pratiques

Travaux pratiques 11 · 1 h

Ajouter la sécurité au pipeline, sans le rendre inutilisable

Brancher trois vérifications sur le pipeline du TP 5, puis trier leurs résultats pour qu'ils restent exploitables.

Avant de commencer

  • Le pipeline du TP 5, qui construit et publie une image
  • Les outils trivy et gitleaks, ou leurs équivalents

Énoncé

  1. Analyser les dépendancesLancez une analyse de composition sur le projet et comptez les vulnérabilités remontées. Notez le nombre : c'est lui qui pose le problème du chapitre.
  2. Trier par exploitabilitéPour les dix plus graves, répondez aux trois questions : le code vulnérable est-il atteignable, est-il dans l'image de production, est-il exploitable à distance ? Comparez le classement obtenu à celui par gravité.
  3. Analyser l'imageAnalysez l'image du TP 3, puis la même application construite sur une base complète. Comparez le nombre de vulnérabilités héritées. Indice : Vous n'aurez modifié qu'une ligne : celle du FROM.
  4. Empêcher les secrets d'entrerAjoutez un crochet local et une étape de pipeline qui refusent une modification contenant un secret. Testez avec une fausse clé, puis avec une référence à une variable d'environnement — la seconde ne doit PAS déclencher d'alerte.
  5. Mesurer les faux positifsPassez le détecteur sur tout l'historique du dépôt. Comptez les alertes et la part réellement problématique. Que se passerait-il si vous laissiez ce niveau de bruit ?
  6. Auditer les droits du pipelineListez ce que votre pipeline a le droit de faire. Retirez tout ce dont il n'a pas besoin sur une demande de fusion — notamment le droit de déployer en production.
  7. Écrire une règle de conformitéAjoutez une vérification automatique qui refuse tout manifeste déclarant un conteneur tournant en root. Testez-la sur un manifeste fautif.

C'est réussi quand

  • Le pipeline échoue sur une fausse clé et passe sur une référence à une variable
  • Vous savez nommer les trois ou quatre vulnérabilités qui méritent une action cette semaine
  • Une demande de fusion ne peut plus déployer en production
  • Un manifeste sans utilisateur non privilégié est refusé

Correction

Les trois vérifications, dans le pipeline.github/workflows/securite.yml
jobs:
secrets:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with: { fetch-depth: 0 }     # tout l'historique
    - run: gitleaks detect --redact --exit-code 1

dependances:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - run: trivy fs --severity HIGH,CRITICAL --exit-code 1 .

image:
  needs: [secrets, dependances]
  runs-on: ubuntu-latest
  steps:
    - run: docker build -t tickets:test .
    - run: trivy image --severity CRITICAL --exit-code 1 tickets:test

Chaque étape porte une vérification, et aucune n'attend la fin — c'est décaler vers la gauche. Le job « secrets » lit tout l'historique : un secret retiré au commit suivant y est toujours.

Ce que le tri change
par gravité             par exploitabilité
1. lodash        9,8     1. express       6,5  point d'entrée public
2. webpack       8,1     2. jsonwebtoken  7,2  signature contournable
3. postgres      7,5     3. postgres      7,5  accès local requis
4. jsonwebtoken  7,2     …  lodash        9,8  fonction jamais appelée
                       …  webpack       8,1  hors image de production

Les deux premières places changent. Le score de gravité est calculé DANS L'ABSOLU, sans connaître votre contexte : une note de 9,8 sur du code que vous n'appelez jamais est moins urgente qu'une note de 6,5 sur votre point d'entrée. Le tri ramène couramment deux cents alertes à quatre qui méritent une action — donc qui seront réellement traitées.

L'image de base, une seule ligne
FROM node:20         →  17 vulnérabilités hautes, 412 paquets, 1,1 Go
FROM node:20-alpine  →   1 vulnérabilité haute,  47 paquets, 130 Mo

Aucune correction de code, aucune montée de version applicative : seize vulnérabilités hautes disparaissent parce que les paquets qui les portaient n'ont jamais eu de raison d'être là. C'est le meilleur rapport effort/résultat de tout le chapitre, et c'est déjà ce que le TP 3 avait fait pour la taille.

Détection de secrets : le réglage compte.gitleaks.toml
[allowlist]
description = "faux positifs connus, justifiés un par un"
regexes = [
"process[.]env[.][A-Z_]+",   # une référence, pas une valeur
"[$]2[aby][$][0-9]{2}[$]",   # une empreinte bcrypt, pas un secret
]
paths = ["tests/fixtures/"]    # jeux d'essai, clés factices

Un détecteur qui crie sur chaque empreinte et chaque exemple est désactivé au bout d'une semaine, et ne protège plus de rien — c'est le bruit d'alertes du chapitre 10. Les exceptions se justifient une par une, en commentaire, et se relisent : une liste d'exceptions qui grossit sans être relue redevient un trou.

Droits du pipeline et règle de conformité
# droits minimaux sur une demande de fusion
permissions:
contents: read
pull-requests: write
# AUCUN droit de déploiement : réservé au job déclenché sur main

# règle de conformité, refusée avant application
deny[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "le conteneur doit tourner sous un utilisateur non privilégié"
}

Le pipeline est souvent l'acteur le plus privilégié de l'organisation : le compromettre dispense d'attaquer la production. Restreindre par branche et par environnement est la mesure la plus rentable. Et une exigence écrite dans un document est une intention — écrite dans le pipeline, c'est une garantie.

Ce que ce semestre laisse

Onze chapitres plus tôt, le problème était une contradiction d'organisation : livrer vite d'un côté, ne rien casser de l'autre.

Le trajet a suivi celui d'une modification de code. Elle est écrite dans un dépôt dont la stratégie de branchement décide de la taille des lots. Elle est empaquetée avec tout ce dont elle a besoin, ce qui rend « ça marche sur ma machine » sans objet. Elle est vérifiée par un pipeline qui échoue vite, puis livrée par une stratégie qui limite le rayon d'impact. Elle atterrit sur une infrastructure décrite dans des fichiers plutôt que construite à la main, qu'un orchestrateur maintient dans l'état déclaré. Et elle est observée, mesurée, défendue.

Ce qu'il faut en garder tient peut-être en trois idées.

Les petits lots gouvernent tout. Fréquence de livraison, taille des branches, granularité des demandes de fusion, mises à jour de dépendances : le même raisonnement revient à chaque chapitre, et il explique à lui seul pourquoi vitesse et stabilité vont ensemble.

On ne cherche pas à empêcher les incidents, mais à en réduire l'ampleur et la durée. Le canari, le retour arrière, l'auto-réparation et le budget d'erreur disent tous la même chose : la panne aura lieu, et ce qui se mesure est ce qu'elle coûte.

Le bruit détruit le signal. Un test instable, une alerte non actionnable, trois cents vulnérabilités non triées produisent tous le même effet — une équipe qui apprend à ignorer, et qui rate ce qui compte. Ce n'est pas une question d'outil mais d'exigence sur ce qu'on accepte d'afficher en rouge.

À retenir

Flashcards · 5 cartes

Que signifie décaler la sécurité vers la gauche, et sur quel argument ?
La répartir tout au long du flux — éditeur, demande de fusion, pipeline, exécution — plutôt que de la concentrer en une porte finale. L'argument est ÉCONOMIQUE : le coût de correction croît fortement avec le retard de la détection, d'un facteur 1 à l'écriture à 60 en production et bien davantage après exploitation. Les ordres de grandeur varient selon les études, la forme de la courbe non. C'est la boucle de rétroaction du chapitre 1, appliquée à la sécurité.
Comment trier des centaines d'alertes de vulnérabilités ?
Par EXPLOITABILITÉ, pas par score de gravité — celui-ci est calculé dans l'absolu, sans connaître votre contexte. Trois questions, dans l'ordre : 1) LE CODE VULNÉRABLE EST-IL ATTEIGNABLE depuis le vôtre ? — le filtre le plus efficace. 2) EST-IL DANS L'IMAGE DE PRODUCTION, ou seulement dans une dépendance de développement écartée par la construction multi-étapes ? 3) EST-IL ACCESSIBLE DE L'EXTÉRIEUR ? Sans ce tri, l'équipe ignore les 280 alertes et perd aussi les graves.
Quelles mesures réduisent le risque lié aux dépendances et aux images ?
DÉPENDANCES : verrouiller les versions (fichier de verrouillage committé, pour que la mise à jour soit une décision et non un effet de bord) et METTRE À JOUR RÉGULIÈREMENT EN PETITS LOTS — une montée par semaine est infiniment moins risquée qu'une migration de deux ans de retard un jour d'urgence. IMAGES : réduire la surface (alpine, distroless : un ordre de grandeur de paquets en moins, donc de vulnérabilités), ne pas tourner en root, et ANALYSER L'IMAGE dans le pipeline — la base vieillit toute seule, sans qu'aucune ligne n'ait changé.
Quelles sont les trois couches qui empêchent un secret d'entrer dans le dépôt ?
1) AVANT LE COMMIT : un crochet local — rapide, mais contournable et dépendant de chaque poste. 2) DANS LE PIPELINE : analyse systématique de la demande de fusion — non contournable, c'est la couche qui compte, mais elle intervient après que le secret est parti sur le serveur, donc la rotation reste nécessaire. 3) À L'EXÉCUTION : jamais dans l'image, injecté au démarrage, et de préférence depuis un coffre-fort qui apporte journalisation, rotation et DURÉE DE VIE LIMITÉE.
Pourquoi le pipeline est-il une cible, et comment le protéger ?
Parce qu'il est souvent l'acteur LE PLUS PRIVILÉGIÉ de l'organisation : il construit, signe, déploie en production, lit les secrets de tous les environnements et modifie l'infrastructure. Le compromettre dispense d'attaquer la production. Trois mesures : restreindre PAR BRANCHE et PAR ENVIRONNEMENT (une demande de fusion n'a aucune raison de pouvoir déployer en production), préférer des identifiants ÉPHÉMÈRES à des jetons permanents, et journaliser. Même principe pour les comptes de service : lire une base ne demande pas les droits d'écriture.