Aller au contenu
Culture DevOps high

Pipeline CI/CD sécurisé : gates, secrets, signature

36 min de lecture

Cette page vous montre comment construire un pipeline CI/CD (intégration et déploiement continus) réellement sécurisé : quatre piliers concrets (security gates, secrets, signature, isolation) plus les attestations de provenance. Elle s'adresse aux équipes DevOps/DevSecOps qui ont déjà un pipeline fonctionnel mais veulent combler ses angles morts avant qu'un attaquant ne les trouve. En 2025, deux attaques ont rappelé pourquoi ce sujet reste urgent : la compromission de tj-actions/changed-files (23 000 dépôts touchés) et le ver Shai-Hulud sur npm. Les deux ont exploité exactement les failles décrites ici.

  • Identifier pourquoi le pipeline CI/CD est devenu une cible de choix pour les attaquants.
  • Mettre en place des security gates qui bloquent le code vulnérable avant la production.
  • Gérer les secrets sans jamais les exposer dans le code ou les logs.
  • Signer et vérifier des artefacts avec Sigstore/cosign pour garantir leur intégrité.
  • Isoler les runners pour limiter l'impact d'une compromission.
  • Générer des attestations de provenance (SLSA, GitHub Attestations) pour prouver comment un artefact a été construit.

Analogie : imaginez une chaîne de production automobile. Vous ne laissez pas n'importe qui entrer dans l'usine (isolation). Vous vérifiez chaque pièce avant assemblage (security gates). Vous protégez les plans secrets (gestion des secrets). Et vous apposez une plaque VIN unique pour prouver l'origine de chaque voiture (signature). Si un seul maillon est défaillant, toute la chaîne est compromise.

Un pipeline CI/CD est votre usine logicielle : il transforme du code source en artefacts déployés en production. Chaque étape est une opportunité pour un attaquant d'injecter du code malveillant, de voler des credentials, de substituer un artefact ou de pivoter vers vos systèmes internes.

Pourquoi le pipeline est une cible privilégiée ? Parce qu'il concentre trois caractéristiques irrésistibles pour un attaquant :

  1. Accès aux secrets : le pipeline détient des tokens pour déployer en production, des clés API, des credentials AWS/Azure/GCP.
  2. Privilèges élevés : il peut modifier le code, publier des artefacts, déployer des infrastructures.
  3. Exécution automatique : compromettre le pipeline transforme chaque commit en vecteur d'attaque automatisé.

Deux incidents illustrent ce risque à des échelles différentes. En 2020, l'attaque SolarWinds a compromis 18 000 organisations en injectant du code malveillant dans le processus de build d'un logiciel légitime : les clients téléchargeaient une version backdoorée sans le savoir. En mars 2025, l'action GitHub tj-actions/changed-files a été détournée après le vol des identifiants d'un mainteneur : l'attaquant a redirigé les tags de version (v1 à v44.5.1) vers un commit malveillant qui exfiltrait les secrets directement dans les logs CI de plus de 23 000 dépôts. Unit 42 de Palo Alto Networks a relié cet incident à une compromission chez Coinbase. La méthode a changé, la cible reste la même : le pipeline.

Voici les fondations non négociables d'un pipeline sécurisé :

PilierObjectifMenace adressée
Security gatesBloquer le code vulnérable avant déploiementCode malveillant, vulnérabilités connues
Gestion des secretsProtéger les credentials et tokensFuite de secrets, accès non autorisé
Signature d'artefactsGarantir l'intégrité et la provenanceTampering, supply chain attack
IsolationLimiter l'impact d'une compromissionMouvement latéral, élévation de privilèges

Pourquoi ces quatre piliers sont indissociables ? Imaginez un pipeline avec des security gates parfaits mais qui expose ses secrets en clair dans les logs : les gates détecteront les CVE, mais l'attaquant aura déjà volé vos clés AWS. C'est exactement le mode opératoire de tj-actions : l'action modifiée ne cassait aucun test, elle se contentait de dumper les secrets dans les logs, invisible aux gates classiques. La sécurité est une chaîne : le maillon le plus faible détermine la solidité de l'ensemble.

Analogie : dans un aéroport, vous passez plusieurs contrôles avant d'embarquer : vérification d'identité au comptoir, contrôle de sécurité, vérification à la porte. Si vous échouez à n'importe quelle étape, vous ne montez pas dans l'avion. Chaque contrôle est un gate qui bloque la progression si les critères ne sont pas remplis.

Les security gates appliquent ce principe au pipeline CI/CD. Ce sont des points de décision automatisés : si le code contient des vulnérabilités critiques, des secrets exposés, ou échoue aux tests de sécurité, le pipeline s'arrête. Pas de déploiement en production.

Le principe du fail fast : pourquoi bloquer tôt plutôt que de corriger en production ? Parce que corriger une vulnérabilité en prod coûte largement plus cher qu'en développement (rollback, gestion d'incident, communication client, perte de confiance). Un gate qui bloque un commit vulnérable vous évite des semaines de travail correctif.

Un pipeline robuste place des gates à chaque transition, comme des sas de sécurité successifs. Si un gate est contourné (erreur de config, bug de l'outil), le gate suivant rattrape l'erreur.

Voici l'architecture de référence, avec le rôle précis de chaque niveau :

GatePositionCe qu'il vérifieAction si échec
Pre-commitAvant le commitSecrets, formatageBloque le commit
PR GateÀ l'ouverture de PRSAST, SCA, testsBloque le merge
Build GateAprès compilationScan d'image, signatureBloque la publication
Deploy GateAvant déploiementApprobation, checks finauxBloque le déploiement

Chaque gate a un rôle spécifique dans le cycle de vie du code. Le pre-commit donne un feedback immédiat au développeur (quelques secondes), avant même de polluer l'historique Git. Le PR Gate apporte une validation collaborative : l'équipe voit les problèmes avant le merge et protège la branche main. Le Build Gate scanne les artefacts finaux (image Docker), car certaines vulnérabilités n'apparaissent qu'après compilation. Le Deploy Gate est la dernière ligne de défense, avec vérification manuelle si nécessaire.

Le dilemme : si vous bloquez le pipeline pour chaque alerte, y compris les faux positifs et les vulnérabilités mineures, les développeurs perdront confiance et chercheront à contourner les gates. Si vous ne bloquez rien, les gates sont inutiles. Il faut trouver l'équilibre entre sécurité et vélocité.

Une stratégie de seuils graduée répond à trois questions : quelle sévérité bloque (CRITICAL toujours, HIGH parfois, MEDIUM presque jamais) ; quel gate bloque quoi (pre-commit bloque peu, deploy gate bloque beaucoup) ; quelle tolérance au faux positif appliquer (les secrets sont binaires, présent ou absent, les CVE nécessitent du contexte).

# Exemple de stratégie de seuils
security_gates:
pre-commit:
block_on: [secrets] # Seuls les secrets bloquent (feedback en quelques secondes)
warn_on: [high_cvss] # Avertir, mais ne pas bloquer (trop tôt)
pr_gate:
block_on: [critical_cvss, secrets, high_cvss_with_exploit]
warn_on: [high_cvss, medium_cvss] # Visible dans la PR, pas bloquant
deploy_gate:
block_on: [any_critical, missing_signature] # Tolérance zéro
require_approval_for: [high_cvss] # Humain décide si contexte complexe

Exemple concret : une CVE HIGH est détectée sur une dépendance Node.js. Le PR gate avertit (commentaire dans la PR : "cette dépendance a une CVE HIGH, mettre à jour vers la version corrigée"). Le développeur peut merger si urgent, mais le deploy gate bloquera le déploiement tant que la correction n'est pas mergée. Résultat : pas de friction inutile, mais garantie que la prod reste saine.

Cet exemple applique trois réflexes issus de la compromission tj-actions : permissions minimales au niveau workflow, actions pinnées par SHA de commit (pas par tag mutable) et persist-credentials: false sur le checkout pour ne pas laisser traîner de token exploitable.

name: Security Gates
on:
pull_request:
branches: [main]
permissions: {} # Rien par défaut, chaque job déclare ce dont il a besoin
jobs:
# Gate 1 : Secrets
secrets-scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # Historique complet pour scanner les commits
persist-credentials: false
- name: Detect secrets
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Gate 2 : SAST
sast:
runs-on: ubuntu-latest
permissions:
contents: read
container:
image: semgrep/semgrep@sha256:59fbed6127ea7c5dde3ba6a85142733bb20ea9aaa36120c953904f1539aaf66e # 1.168.0
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: SAST scan
run: semgrep scan --config p/security-audit --config p/owasp-top-ten --error
# Gate 3 : SCA
sca:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Dependency scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
exit-code: '1'
severity: 'CRITICAL'
# Gate final : tous les checks doivent passer
security-gate:
needs: [secrets-scan, sast, sca]
runs-on: ubuntu-latest
steps:
- run: echo "All security gates passed"

Le point d'attention le plus souvent oublié : returntocorp/semgrep-action, longtemps recommandée, est dépréciée depuis 2024 au profit d'un appel direct au binaire Semgrep. Vérifiez toujours qu'une action tierce est activement maintenue avant de la pinner dans un pipeline critique.

Scénario réel : un développeur a committé par erreur une clé API AWS dans un dépôt GitHub public. En quelques minutes, un bot automatisé a détecté la clé, l'a utilisée pour créer des instances EC2 de minage de cryptomonnaie, et a généré une facture de plusieurs dizaines de milliers d'euros en une nuit. Le développeur a découvert le problème en recevant une alerte de dépassement de budget AWS.

Les secrets (API keys, tokens, passwords, certificats) sont le nerf de la guerre du pipeline : ils donnent accès à vos systèmes de production, vos bases de données, vos fournisseurs cloud. Un seul secret exposé peut suffire à une compromission complète.

Pourquoi les secrets fuient-ils si souvent ? Parce qu'ils sont partout : dans le code (hardcodés), dans les variables d'environnement (loggées), dans les fichiers de config (versionnés), dans les containers (visibles avec docker inspect). Chaque endroit est une opportunité de fuite. En septembre 2025, le ver Shai-Hulud a exploité exactement cette surface : propagé sur plus de 500 paquets npm compromis, il scannait automatiquement les environnements CI à la recherche de tokens GitHub, credentials AWS/GCP/Azure et clés npm, puis se republiait lui-même via les comptes de mainteneurs volés pour se propager plus loin. L'histoire ne s'est pas arrêtée là : une seconde vague (« Shai-Hulud 2.0 ») a touché près de 800 paquets supplémentaires fin novembre 2025, preuve que ce type de ver auto-propagateur reste une menace active plutôt qu'un incident isolé.

Les erreurs classiques (et comment elles se produisent)

Section intitulée « Les erreurs classiques (et comment elles se produisent) »

Voici les patterns d'erreur les plus fréquents, avec des exemples concrets. Ce tableau sert de checklist rapide avant chaque revue de pipeline : chaque ligne correspond à une fuite réellement observée en production, pas à un risque théorique.

ErreurRisqueFréquenceExemple concret
Secret en variable d'environnement non masquéeVisible dans les logsTrès couranteecho $DATABASE_PASSWORD apparaît dans les logs CI, potentiellement indexé publiquement
Secret hardcodé dans le codeExposé dans l'historique GitCouranteconst API_KEY = "sk_live_abc123", même supprimé, reste dans l'historique Git
Secret partagé entre environnementsCompromission transversaleCouranteMême token AWS pour dev/prod : compromission dev devient compromission prod
Secret sans rotationExploitation prolongée après fuiteTrès couranteClé API créée il y a des années, jamais changée : si elle fuite, elle reste valide indéfiniment
Secret avec privilèges excessifsImpact amplifiéCouranteToken admin au lieu de read-only : vol du token égale contrôle total

Pourquoi ces erreurs persistent-elles ? Parce que les secrets sont invisibles jusqu'à ce qu'ils fuient. Un développeur qui hardcode une clé API "temporairement" oublie souvent de la retirer. Une variable non masquée passe inaperçue jusqu'au jour où un attaquant fouille vos logs publics. La sécurité des secrets exige de la discipline et de l'automatisation.

Principes de gestion sécurisée : les cinq règles d'or

Section intitulée « Principes de gestion sécurisée : les cinq règles d'or »

Prémisse : la gestion des secrets repose sur une règle absolue, le code ne doit jamais contenir de valeurs secrètes, seulement des références à des secrets stockés ailleurs. Un secret dans le code est une bombe à retardement.

Voici les cinq principes non négociables, avec la justification de chacun :

  1. Jamais de secrets dans le code : externaliser systématiquement

    Pourquoi ? Le code est versionné dans Git. Même si vous supprimez le secret dans un commit ultérieur, il reste dans l'historique. N'importe qui avec accès au dépôt (anciens employés, forks publics, mirrors) peut fouiller l'historique et extraire le secret.

    Utilisez toujours un gestionnaire de secrets externe. Le code ne doit contenir que des références.

    # JAMAIS
    env:
    DATABASE_PASSWORD: "SuperSecret123!"
    # TOUJOURS
    env:
    DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
  2. Moindre privilège : limiter le blast radius

    Pourquoi ? Si un secret fuite, et cela arrivera, l'impact doit être minimal. Un token admin permet de tout faire : supprimer des dépôts, modifier des secrets, créer des backdoors. Un token read-only permet juste de lire du code.

    Exemple concret : votre pipeline doit publier un package npm. Créez un token avec uniquement la permission publish, pas admin. Si le token fuite, l'attaquant peut publier des versions malveillantes (détectable), pas supprimer votre compte npm (irréversible).

    # Trop permissif
    permissions:
    contents: write # Peut modifier le code
    packages: write
    actions: write # Peut modifier les workflows
    # Minimal
    permissions:
    contents: read # Juste lire le code
    packages: write # Uniquement publier des packages
  3. Isolation par environnement : cloisonner les contextes

    Pourquoi ? Un développeur qui teste localement ou en dev ne devrait jamais avoir besoin des secrets de production. Si vous partagez les mêmes secrets, une erreur en dev (logger le secret, commit accidentel) compromet la prod.

    Utilisez des secrets différents pour dev, staging et production, jamais de secret prod en dev :

    jobs:
    deploy-dev:
    environment: development
    steps:
    - name: Deploy
    env:
    API_KEY: ${{ secrets.DEV_API_KEY }} # Token avec accès au sandbox
    deploy-prod:
    environment: production # Environnement GitHub avec validation manuelle
    steps:
    - name: Deploy
    env:
    API_KEY: ${{ secrets.PROD_API_KEY }} # Token prod, jamais accessible en dev

    Dans GitHub, les environnements production peuvent exiger une approbation manuelle avant déploiement, une double sécurité simple à activer.

  4. Rotation automatique : limiter la fenêtre d'exploitation

    Pourquoi ? Vous ne saurez jamais avec certitude quand un secret fuite. La rotation réduit la fenêtre d'exploitation : si vous changez vos secrets tous les 90 jours, un secret volé il y a 91 jours est inutilisable.

    Les secrets doivent être renouvelés régulièrement, idéalement automatiquement via un gestionnaire dédié :

    Type de secretFréquence de rotationJustification
    API keys90 joursCompromis entre sécurité et complexité opérationnelle
    Tokens d'accès24h, préférer des tokens éphémèresMinimiser l'impact d'une fuite
    CertificatsAvant expiration, automatiséEx. Let's Encrypt renouvelé via Cert-Manager
    Passwords de service180 joursChangement manuel plus complexe, à automatiser progressivement

    Préférez les tokens éphémères générés à la demande (OIDC, IAM Roles for Service Accounts) plutôt que des secrets statiques. GitHub Actions peut s'authentifier sur AWS/Azure/GCP via OIDC sans aucun secret stocké.

  5. Audit et monitoring

    Chaque accès à un secret doit être loggé et auditable. C'est ce qui a permis, dans plusieurs incidents récents, de détecter une fuite en quelques heures plutôt qu'en plusieurs mois.

Solutions de gestion de secrets : choisir selon la maturité

Section intitulée « Solutions de gestion de secrets : choisir selon la maturité »

Le dilemme : il existe des dizaines d'outils de gestion de secrets. Commencez simple, complexifiez au besoin. Ne déployez pas un outil enterprise complexe si vous avez deux secrets et un seul pipeline GitHub.

Voici un guide de sélection pragmatique, du plus simple au plus sophistiqué :

SolutionCas d'usageComplexitéQuand l'utiliser
GitHub SecretsProjets simples, GitHub-centricFaiblePetite équipe, infrastructure sur GitHub Actions uniquement
SOPSSecrets versionnés avec le code (chiffrés)MoyenneBesoin de versionner les secrets avec le code (GitOps), multi-cloud
HashiCorp VaultEnterprise, multi-cloud, rotation autoÉlevéeGrande organisation, conformité stricte, rotation automatique essentielle
InfisicalAlternative open source à VaultMoyenneBudget limité, besoin de features Vault sans le coût

Le choix dépend surtout de votre volume de secrets et de votre surface multi-cloud : moins de dix secrets sur une seule plateforme CI, restez sur les secrets natifs. Des besoins de versionnement GitOps orientent vers SOPS. Une exigence d'audit strict et de rotation automatique pousse vers Vault ou une alternative open source comme Infisical.

Signature d'artefacts : prouver l'origine et l'intégrité

Section intitulée « Signature d'artefacts : prouver l'origine et l'intégrité »

Analogie : quand vous achetez un médicament, vous vérifiez le sceau inviolable du bouchon. Ce sceau prouve deux choses, personne n'a ouvert le flacon (intégrité), et il provient bien du laboratoire officiel (provenance). Si le sceau est brisé ou absent, vous ne consommez pas le produit.

La signature cryptographique d'artefacts logiciels repose sur le même principe. Elle garantit l'intégrité (l'artefact n'a pas été modifié depuis sa création) et la provenance (l'artefact provient bien du pipeline autorisé, pas d'un attaquant).

Scénario d'attaque sans signature : vous buildez une image Docker pour une API de facturation et la publiez dans un registre. Un attaquant compromet vos identifiants (mot de passe faible, pas de 2FA). Il remplace votre image par une version identique en apparence, mais avec un backdoor. Votre pipeline déploie l'image en production : vous venez de déployer le malware de l'attaquant, en contact direct avec des données de paiement. Avec une signature, ce scénario devient impossible : le pipeline vérifie la signature avant déploiement, et l'image substituée n'étant pas signée avec votre clé, le déploiement est refusé.

Pourquoi la signature n'est-elle pas encore généralisée partout ? Historiquement, parce que c'était compliqué : gérer des clés privées, distribuer des clés publiques, maintenir une infrastructure PKI. Sigstore a changé la donne en popularisant la signature keyless : plus de clé privée à gérer, juste votre identité OIDC (GitHub, GitLab, Google).

Dans une architecture moderne, les artefacts traversent de nombreuses étapes :

Code → Build → Registry → Deploy → Production

À chaque transition, un attaquant pourrait intercepter et modifier l'artefact. La signature crée une chaîne de confiance vérifiable à chaque étape, plutôt qu'une confiance implicite dans le registre ou le réseau.

Sigstore est devenu le standard de facto pour la signature d'artefacts dans l'écosystème cloud-native (adopté par Kubernetes, Google, Red Hat, GitHub). Il résout le problème historique de la signature : la gestion des clés privées.

Avant Sigstore : pour signer, vous deviez générer une paire de clés GPG/RSA, la stocker de manière sécurisée (HSM, KMS), distribuer la clé publique, gérer la rotation, révoquer si compromise. Résultat : l'immense majorité des projets ne signaient rien.

Avec Sigstore : vous vous authentifiez via OIDC (GitHub Actions, GitLab CI), Sigstore émet un certificat éphémère valide quelques minutes, vous signez, le certificat expire. Pas de clé privée à gérer, mais une traçabilité complète via un log de transparence public, Rekor.

Voici les trois composants de l'écosystème :

ComposantRôleAnalogieCe qu'il fait concrètement
CosignSigner et vérifier les images/artefactsLe stylo qui signeCLI utilisée pour cosign sign, cosign verify
FulcioCA qui émet des certificats éphémèresLe notaire qui certifie l'identitéVérifie l'identité OIDC, émet un certificat X.509 de quelques minutes
RekorLog de transparence (audit trail)Le registre public des signaturesEnregistre chaque signature dans un ledger immuable, vérifiable par tous

Pourquoi Rekor est-il crucial ? Parce qu'il rend les attaques détectables : si un attaquant compromet votre CI et signe un artefact malveillant, la signature est enregistrée publiquement dans Rekor avec l'identité utilisée. Impossible de signer en secret. Depuis octobre 2025, Rekor v2 est disponible en production avec un nouveau backend basé sur Trillian-Tessera, plus scalable pour les gros volumes de signatures ; les versions récentes de cosign basculent automatiquement dessus.

  1. Build : créer l'artefact

    Fenêtre de terminal
    docker build -t registry.entreprise.io/api-facturation:v1.0.0 .
  2. Push : publier dans le registry

    Fenêtre de terminal
    docker push registry.entreprise.io/api-facturation:v1.0.0
  3. Sign : signer avec Cosign

    Fenêtre de terminal
    # Signature keyless (recommandée)
    cosign sign registry.entreprise.io/api-facturation:v1.0.0
    # Cosign utilise l'identité OIDC (GitHub Actions, GitLab CI)
    # Pas de clé privée à gérer
  4. Verify : vérifier avant déploiement

    Fenêtre de terminal
    cosign verify \
    --certificate-identity "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    registry.entreprise.io/api-facturation:v1.0.0

Voici comment enchaîner les quatre étapes du workflow, build, publication, installation de Cosign, puis signature, dans un seul job GitHub Actions. Le point clé est le champ id-token: write : c'est lui qui permet à Cosign d'obtenir un jeton OIDC auprès de GitHub pour la signature keyless, sans jamais manipuler de clé privée.

name: Build and Sign
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Nécessaire pour la signature keyless
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Build image
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
run: |
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign image
run: |
cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}

Alternative native GitHub : si votre pipeline tourne exclusivement sur GitHub Actions, l'action officielle actions/attest-build-provenance (généralement disponible depuis juin 2024) génère une attestation de provenance directement rattachée à l'artefact, sans installer Cosign séparément. Elle repose sur le même socle Sigstore en coulisses, mais simplifie l'intégration pour un usage 100 % GitHub. Le détail de son fonctionnement est repris dans la section suivante sur les attestations.

Isolation et moindre privilège : limiter le blast radius

Section intitulée « Isolation et moindre privilège : limiter le blast radius »

Le principe du blast radius (rayon de l'explosion) : en sécurité, on part du principe que tout système finira par être compromis. La question n'est pas "si", mais "quand". L'isolation répond à une autre question : quand mon pipeline sera compromis, quel sera l'impact réel ?

Scénario sans isolation : un runner self-hosted tourne sur un serveur avec accès au réseau interne de l'entreprise. Un attaquant injecte du code dans un workflow qui scanne ce réseau, vole les credentials d'une base de données, exfiltre des données clients. Le blast radius couvre toute l'infrastructure interne.

Scénario avec isolation : le runner tourne dans un container éphémère, dans un réseau isolé, sans accès au réseau interne. Le workflow ne peut atteindre qu'Internet et les services explicitement autorisés. Le blast radius se réduit à un container jetable.

Le ver Shai-Hulud (septembre 2025) illustre pourquoi l'isolation compte autant que la détection : il n'exploitait pas une faille technique nouvelle, il profitait simplement de runners qui avaient accès en clair à des credentials AWS, GCP et npm en mémoire pendant l'exécution du job. Un runner correctement isolé, sans accès réseau superflu et détruit après usage, aurait limité sa capacité d'exfiltration.

Voici comment cloisonner un pipeline pour minimiser l'impact d'une compromission. Chaque niveau agit sur un axe différent de l'attaque, et c'est leur combinaison qui construit une défense réellement robuste, pas un seul d'entre eux isolément.

NiveauMesureBénéficeExemple concret
RéseauRunners dans un réseau isoléPas d'accès aux ressources internesRunner dans un réseau dédié, sans route vers la prod
ComputeRunners éphémères (détruits après usage)Pas de persistance d'une compromissionContainer détruit après chaque job, le malware ne persiste pas
PermissionsToken avec moindre privilègeLimite les actions possiblesToken read au lieu de write : vol du token égale lecture seule
EnvironnementsSéparation dev/staging/prodCompromission dev différente de compromission prodClusters et comptes cloud séparés

Pourquoi chaque niveau compte-t-il ? Un runner éphémère avec des permissions excessives peut quand même exfiltrer des secrets pendant sa courte vie. Un runner avec des permissions minimales mais permanent peut servir de point d'entrée pour des attaques futures. La combinaison des quatre crée une défense robuste.

Le problème des runners permanents : un runner self-hosted qui tourne en continu ressemble à un ordinateur partagé dans un cybercafé. Chaque job laisse des traces, fichiers temporaires, historique shell, credentials en mémoire. Le job suivant hérite de cet environnement pollué.

La solution : runners éphémères. Un runner éphémère est créé pour un seul job, puis détruit complètement : filesystem effacé, mémoire libérée, aucune persistance. Le job suivant part d'un environnement vierge, ce qui coupe court à ce type d'attaque en chaîne.

jobs:
build:
runs-on: ubuntu-latest # Runner GitHub hébergé, détruit après le job (recommandé)
# Ou pour self-hosted (si vous devez gérer vos propres runners)
sensitive-deploy:
runs-on: [self-hosted, ephemeral] # Label pour runners éphémères

Les runners GitHub hébergés (ubuntu-latest, windows-latest) sont toujours éphémères. C'est gratuit dans les limites du plan, sécurisé, et sans gestion d'infrastructure. Préférez-les sauf besoin spécifique (GPU, architecture ARM, conformité imposant un hébergement dédié).

Réduire les permissions au strict minimum limite ce qu'un token compromis peut réellement faire, même si un attaquant parvient à l'exfiltrer via un job malveillant.

# Permissions par défaut restrictives
permissions:
contents: read # Lecture du code seulement
jobs:
build:
# Permissions spécifiques au job si nécessaire
permissions:
contents: read
packages: write # Uniquement pour ce job

Attestations et provenance : prouver le processus de build

Section intitulée « Attestations et provenance : prouver le processus de build »

Le problème : une signature prouve que l'artefact est intègre et provient de votre pipeline, mais elle ne dit rien sur comment l'artefact a été construit. Quelles dépendances ? Quel commit Git ? Quel runner ? Quelles variables d'environnement ?

Exemple : votre image Docker est signée, parfait. Mais un développeur malveillant a modifié le Dockerfile pour installer un backdoor avant le build. L'image signée contient le backdoor. La signature est valide, mais l'artefact est compromis dès l'origine.

Les attestations résolvent ce problème en ajoutant des métadonnées vérifiables sur le processus de build : commit source, dépendances utilisées, runner qui a exécuté le build, horodatage. Ces métadonnées sont signées avec l'artefact. Résultat : vous pouvez vérifier comment l'artefact a été construit, pas juste qu'il l'a été.

SLSA : le framework de référence pour la provenance

Section intitulée « SLSA : le framework de référence pour la provenance »

SLSA (Supply-chain Levels for Software Artifacts, prononcé "salsa") définit des niveaux de maturité progressifs pour la sécurité de la supply chain. Chaque niveau ajoute des garanties techniques qui rendent la falsification plus difficile. Depuis la spécification v1.0, le framework est organisé en pistes (tracks) : la piste Build, la plus utilisée en CI/CD, compte quatre niveaux allant de L0 à L3 (l'ancien niveau 4 des versions v0.1 a été retiré lors de cette restructuration).

Pourquoi des niveaux ? Parce que viser directement le niveau maximal est irréaliste pour la majorité des organisations. SLSA permet de progresser étape par étape, avec un objectif atteignable à chaque palier.

Niveau (Build track)ExigencesCe que ça prouveDifficulté de falsification
L0AucuneAucune garantieTriviale
L1Provenance existanteComment le build a été produit, informations de baseFacile (peut être falsifiée par un humain)
L2Provenance générée par une plateforme de build hébergée, signéeBuild reproductible, non modifié après coupMoyenne (nécessite de compromettre la plateforme CI)
L3Build isolé, secrets de signature inaccessibles au code utilisateurIntégrité garantie même si un développeur est malveillantDifficile (nécessite de compromettre la plateforme CI elle-même)

Objectif pragmatique : visez L2 au minimum (atteignable nativement avec GitHub Actions ou GitLab CI), et L3 pour les applications critiques (nécessite une isolation renforcée du build, comme celle qu'offrent les générateurs SLSA officiels).

Deux approches coexistent aujourd'hui. Pour une conformité SLSA L3 stricte, le générateur officiel du projet SLSA reste la référence, car il isole le build dans une infrastructure dédiée hors de votre contrôle direct :

- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ steps.build.outputs.digest }}

Pour un usage plus simple, centré sur GitHub, l'action officielle actions/attest (successeur de attest-build-provenance) génère une attestation directement liée à l'artefact publié, sans infrastructure supplémentaire :

jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
attestations: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Build and push image
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

Cette seconde approche suffit à atteindre un niveau proche de SLSA L2 sans opérer d'infrastructure de build dédiée, ce qui la rend accessible à la majorité des équipes qui débutent sur ce sujet.

Cette checklist sert de fil conducteur pour évaluer où en est votre pipeline aujourd'hui, sans viser une perfection immédiate : elle est volontairement organisée en trois paliers de maturité croissante, du socle indispensable jusqu'aux pratiques réservées aux équipes DevSecOps avancées.

Ces quatre points forment le minimum vital : sans eux, un incident de sécurité n'est pas une question de "si" mais de "quand". Ils se mettent en place en quelques jours sur un pipeline existant, sans réorganisation profonde.

  • Secrets dans un gestionnaire (pas dans le code)
  • Scan de secrets en pre-commit
  • SAST sur les PR
  • SCA sur les dépendances

Ce palier demande davantage de coordination avec les équipes infrastructure (runners, environnements GitHub) mais reste atteignable en quelques semaines pour une équipe qui maîtrise déjà le socle basique.

  • Runners éphémères
  • Permissions minimales
  • Environnements séparés (dev/staging/prod)
  • Signature des images

Ces pratiques ciblent les organisations avec des exigences de conformité fortes ou des artefacts particulièrement sensibles : elles demandent un investissement outillage plus consistant, mais réduisent fortement la surface d'attaque résiduelle.

  • Attestations SLSA ou GitHub Attestations
  • SBOM pour tous les artefacts
  • Vérification des signatures avant déploiement
  • Rotation automatique des secrets
  • Security gates : points de contrôle automatiques à chaque étape (pre-commit, PR, build, deploy), avec le principe du fail fast.
  • Secrets : jamais dans le code, rotation régulière, moindre privilège, tokens éphémères de préférence aux secrets statiques.
  • Signature : Cosign/Sigstore (ou l'action native GitHub) pour garantir l'intégrité et la provenance des artefacts.
  • Isolation : runners éphémères, permissions minimales, environnements séparés, quatre niveaux complémentaires.
  • Attestations : SLSA (L0 à L3 sur la piste Build) ou attestations GitHub pour prouver comment un artefact a été construit.
  • Pinner par SHA, pas par tag : la compromission tj-actions de 2025 a démontré qu'un tag mutable comme @v4 peut être redirigé vers un commit malveillant.
  • La supply chain est désormais un risque de premier plan : l'OWASP Top 10:2025 lui consacre une catégorie dédiée, aux côtés des vulnérabilités applicatives classiques.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn