Jenkins sur Windows Server: ce qu'il ne faut pas faire
J’ai du travailler récemment avec une instance Jenkins un peu spéciale et ce qu’elle m’a appris tient moins à une faille précise qu’à la manière dont une poignée de décisions raisonnables prises isolément finissent, mises bout à bout, par dessiner un chemin tout droit du moindre pipeline jusqu’au contrôleur de domaine. Je raconte le cas parce qu’il est exemplaire, au sens strict: c’est un catalogue de ce qu’il ne faut pas faire et chaque erreur se corrige, mais c’est leur accumulation qui fait le danger, pas aucune d’elles en particulier.
Le décor, pour planter la chose: un controller Jenkins qui tourne sur Windows Server, joint au domaine active-directory, avec des pipelines exécutés sur le controller lui-même. Un Jetty embarqué qui écoute sur le 8080, directement atteignable. Des identifiants de domaine qui transitent en clair vers le Jetty. Pas de mises à jour depuis plusieurs années. Une collection de greffons dont plusieurs n’ont plus de correctif. Et en ajouts récents un nginx posé à côté qui termine le TLS mais ne redirige pas le 80 vers le 443 et surtout ne protège rien tant que le 8080 reste ouvert derrière lui. Le code qui pouvait être syncrhonisé depuis des sources internes ou tierces était executé et/ou build au sein des pipelines sans audit préalable. Pris séparément, chacun de ces points se discute (quoi que). Pris ensemble, ils forment une rampe.
Le controller n’est pas un worker
La première idée reçue à défaire, c’est que le controller puisse accueillir des pipelines.
La documentation Jenkins est explicite là-dessus et je la paraphrase sans la trahir: tout pipeline qui s’exécute sur le built-in node a le même accès au système de fichiers que le processus Jenkins, donc à JENKINS_HOME, aux secrets, aux credentials, à la configuration.
Autrement dit, tant que le nombre d’exécuteurs du built-in node n’est pas à zéro, n’importe quel pipeline vaut potentiellement prise de contrôle du controller et il suffit pour cela d’une dépendance empoisonnée ou d’un Jenkinsfile un peu curieux.
Ce n’est pas une subtilité de durcissement, c’est le premier cran: les pipelines vont sur des agents, le controller orchestre et ne compile rien.
Et puisque la question vient toujours, autant la trancher: monter un controller Jenkins sous Windows pour faire des builds Windows est une idée qu’on croit pratique et qui ne l’est pas. Les pipelines Windows ont besoin d’agents Windows, pas d’un controller Windows. Le controller, lui, n’a aucune raison d’être sur un hôte joint au domaine et c’est précisément cette jointure qui transforme une compromission locale en problème d’annuaire.
Pourquoi chaque facteur aggrave le suivant
Un hôte joint au domaine qui exécute du code arbitraire, c’est une machine depuis laquelle on extrait des secrets en mémoire, on relaie du NTLM, on abuse du compte machine pour se déplacer latéralement. La signature SMB et le channel binding LDAP ne sont pas activés par défaut sur les versions de Windows Server encore très répandues et la renaissance des attaques de coercition et de relais en 2025 et 2026 n’a rien de théorique. Mettez bout à bout “le controller exécute des pipelines non audités” et “le controller est joint au domaine” et vous avez relié la surface la plus exposée de la CI à la surface la plus sensible du SI.
Le 8080 à l’air libre ajoute la couche la plus bête et la plus efficace. Le TLS terminé par nginx ne protège rien si l’on peut parler au Jetty directement en HTTP sur le 8080 et c’est par là que passent, en clair, les identifiants des utilisateurs et des comptes de service qui s’authentifient contre l’annuaire. Il n’y a pas besoin d’exploit pour ce scénario, juste d’une écoute sur le bon segment: le plus trivial des accès et le plus rentable.
Les greffons, enfin, sont chacun une surface. Ceux qu’on n’utilise pas (un connecteur de forge ici, un greffon d’e-mail là) élargissent l’attaque sans rien apporter et ceux qui restent sans correctif sont des portes documentées. Une instance en retard de dix-huit à vingt-quatre mois sur les avis de sécurité n’est pas “un peu datée”, c’est une liste publique de chemins d’entrée.
Les scénarios, du plus bête au plus complet
Je les ai rangés par sophistication croissante et le plus instructif est le premier, parce qu’il ne demande aucun talent.
Le plus trivial: un attaquant se place sur le segment, écoute le trafic vers le 8080 en clair, récupère les identifiants qui s’authentifient contre l’annuaire et les rejoue pour se déplacer. Pas d’exploit, pas de RCE, pas de sophistication, juste le 8080 en clair couplé à l’authentification par annuaire.
Ensuite la supply chain par pipeline: on empoisonne une dépendance ou une étape de build, le pipeline s’exécute sur le controller, le code tourne avec l’accès du processus Jenkins et les secrets suivent. Déclencheur simple, impact majeur.
Puis la chaîne complète vers l’annuaire: un point d’entrée quelconque (greffon vulnérable, CLI exposée, pipeline), l’exécution de code sur l’hôte Windows, l’extraction des credentials en mémoire, le relais ou le pass-the-hash, l’atteinte d’un contrôleur de domaine et de là tout le SI. Le point à retenir n’est pas qu’une étape soit géniale, c’est qu’aucune ne l’est: chacune est documentée et banale et c’est leur enchaînement, rendu possible par l’accumulation décrite plus haut, qui fait la gravité.
Le CVSS ne dit pas le risque
Un détail de méthode, parce qu’il m’a plus d’une fois servi à faire comprendre l’urgence à des gens qui regardaient les scores un par un. Le CVSS d’une faille se calcule hors contexte et le risque réel dépend de trois choses que le score ignore: l’exposition, l’exploitabilité dans cet environnement précis et l’impact une fois replacé dans l’architecture. Un built-in node avec des exécuteurs, un hôte joint au domaine, un 8080 en clair, des greffons sans correctif: aucun n’est forcément “critique” sur l’échelle et leur convergence l’est. C’est l’enchaînement qu’on cote, pas la brique.
Comment on sort de là
Dans l’ordre d’urgence, le court terme d’abord.
Le Jetty en écoute exclusivement sur 127.0.0.1, le reverse proxy comme unique point d’entrée, la redirection du 80 vers le 443 et HSTS.
Les exécuteurs du built-in node à zéro, les builds sur des agents isolés.
La mise à jour de Jenkins et des greffons et la désinstallation de tout ce qui ne sert pas.
La CLI désactivée si personne ne s’en sert.
L’accès réseau au controller restreint aux seules sources qui ont une raison de l’atteindre.
À moyen terme, l’architecture cible et c’est là que le “Windows joint au domaine” disparaît pour de bon. Une instance Linux dédiée, immuable, hors domaine, qui fait tourner la CI et son reverse proxy de façon déclarative, sur des images de base durcies, scannées, signées. Des agents de build éphémères et isolés. Une authentification moderne, OIDC ou SSO plutôt que NTLM et LDAP. Les secrets externalisés dans un vault. Et une segmentation stricte entre la CI et le reste du SI, pour que même une compromission du controller ne soit plus un billet pour l’annuaire.
Un mot sur le réflexe “on change d’outil”. Migrer vers un autre moteur de CI ne corrige rien si l’architecture de déploiement reste la même: un hôte joint au domaine, des builds non isolés, une exposition réseau mal pensée. Le sujet n’a jamais été l’outil, il a toujours été le modèle de déploiement et l’isolation.
La morale, s’il en faut une
Aucune des erreurs de ce cas n’est exotique et c’est bien ce qui le rend utile. Un controller outdated qui compile, un hôte joint au domaine, un port en clair, des greffons oubliés: pris un par un, on les range dans la case “à voir plus tard”. Mis bout à bout, ils relient le pipeline le plus anodin au cœur du SI et le travail de durcissement consiste moins à colmater une faille héroïque qu’à casser l’enchaînement, en commençant par le maillon le moins cher à retirer, c’est-à-dire le 8080 qu’on ferme et les exécuteurs qu’on met à zéro.
[edit: 2026] L’implémentation Ansible de tout ceci: le rôle jenkins-agent .