Un pipeline CI/CD dispose souvent d'un accès privilégié à des ressources sensibles : code source, registres, identités de déploiement, secrets applicatifs ou accès cloud temporaires. C'est la cible parfaite pour une attaque supply chain. Et pourtant, c'est souvent le dernier endroit qu'on pense à sécuriser.
Le hardening (durcissement) de l'environnement de build consiste à réduire la surface d'attaque de vos pipelines CI/CD : runners, secrets, réseau, cache, et processus de build. C'est un prérequis pratique pour progresser vers SLSA, en particulier à partir du moment où l'on vise une plateforme de build maîtrisée et, plus encore, des hardened builds de niveau 3.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Les vecteurs d'attaque sur les environnements de build
- L'architecture défensive : runners éphémères, isolation, egress control
- La gestion des secrets : OIDC, tokens courts, rotation automatique
- Le durcissement pas à pas de GitHub Actions et GitLab CI
Prérequis
Section intitulée « Prérequis »- Connaissance de base des pipelines CI/CD (GitHub Actions ou GitLab CI)
- Lecture recommandée : Attaques sur les pipelines
Pourquoi sécuriser l'environnement de build ?
Section intitulée « Pourquoi sécuriser l'environnement de build ? »Un poste de développement compromis touche un développeur ; un runner CI/CD compromis touche tout ce que le pipeline signe, publie et déploie. Les trois sous-sections qui suivent posent le raisonnement dans cet ordre : ce que le pipeline détient réellement comme pouvoirs, par quelles portes un attaquant entre, et ce que cela a déjà produit en conditions réelles. Sans ce cadrage, les contre-mesures ressemblent à une checklist arbitraire.
Le pipeline : une cible de choix
Section intitulée « Le pipeline : une cible de choix »L'environnement de build est un actif critique car il :
| Caractéristique | Risque associé |
|---|---|
| A accès aux secrets de production | Exfiltration de credentials |
| Peut modifier le code (merge, tag) | Injection de backdoors |
| Signe les artefacts | Signature d'artefacts compromis |
| Publie vers les registres | Distribution de malware |
| Exécute du code tiers (actions, packages) | Exécution de code malveillant |
Chaque ligne se cumule aux autres : un pipeline réunit dans un même contexte d'exécution ce qu'une organisation sépare habituellement en plusieurs équipes et plusieurs comptes. La conséquence est que la compromission d'un seul job donne souvent tout, sans avoir besoin d'un rebond supplémentaire.
La surface d'attaque d'un pipeline
Section intitulée « La surface d'attaque d'un pipeline »Le schéma ci-dessous recense les six zones par lesquelles un attaquant peut entrer. Lisez-le de gauche à droite : chaque flèche représente une relation de confiance que rien ne vérifie par défaut, du dépôt vers le runner, du runner vers le registre. Les deux zones les plus souvent négligées sont le cache (réutilisé entre exécutions sans contrôle d'intégrité) et l'egress réseau, laissé totalement ouvert sur la quasi-totalité des runners.
┌─────────────────────────────────────────────────────────────────────────────┐│ SURFACE D'ATTAQUE D'UN PIPELINE CI/CD │├─────────────────────────────────────────────────────────────────────────────┤│ ││ ┌───────────────────┐ ┌───────────────────┐ ┌──────────────────┐ ││ │ CODE SOURCE │ │ RUNNER │ │ ARTEFACTS │ ││ │ │ │ │ │ │ ││ │ - Repo compromis │───▶│ - VM persistante │───▶│ - Image signée │ ││ │ - PR malveillante │ │ - Secrets exposés │ │ - Registre public│ ││ │ - Workflow modifié│ │ - Réseau ouvert │ │ - Cache empoisonné│ ││ └───────────────────┘ └───────────────────┘ └──────────────────┘ ││ │ ││ ▼ ││ ┌───────────────────┐ ┌───────────────────┐ ┌──────────────────┐ ││ │ DÉPENDANCES │ │ ACTIONS/JOBS │ │ RÉSEAU │ ││ │ │ │ │ │ │ ││ │ - npm/pip/maven │───▶│ - Actions tierces │───▶│ - Egress libre │ ││ │ - Images base │ │ - Plugins non vér.│ │ - C2 possible │ ││ │ - Cache compromis │ │ - Permissions trop│ │ - Exfiltration │ ││ └───────────────────┘ └───────────────────┘ └──────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘Exemples d'attaques réelles
Section intitulée « Exemples d'attaques réelles »Ces cinq incidents sont documentés publiquement et couvrent une décennie. Regardez surtout la colonne Vecteur : aucun ne repose sur une faille du produit CI/CD lui-même, tous exploitent une confiance accordée à un script, à un mainteneur ou à une action tierce. C'est le point important pour la suite, le durcissement porte sur les entrées du build, pas sur le durcissement du produit CI.
| Attaque | Vecteur | Impact |
|---|---|---|
| Codecov (2021) | Script bash modifié dans l'image de build | Exfiltration de secrets CI vers l'attaquant |
| Event-stream (2018) | Mainteneur compromis → code malveillant | Vol de Bitcoin via dépendance npm |
| SolarWinds (2020) | Compromission du build system | Backdoor dans 18 000 organisations |
| tj-actions (2025) | Action GitHub compromise | Exfiltration de secrets |
| xz-utils (2024) | Build scripts modifiés | Backdoor SSH dans binaires |
Architecture défensive
Section intitulée « Architecture défensive »Les mesures qui suivent sont indépendantes : chacune se met en place seule et apporte un gain mesurable, sans attendre que les six autres soient en production. C'est ce qui rend le durcissement CI/CD abordable, contrairement à une refonte d'architecture. Elles se complètent cependant, un runner éphémère sans réduction des permissions laisse toujours un job unique exfiltrer un token de déploiement.
Les 7 piliers du hardening
Section intitulée « Les 7 piliers du hardening »Le schéma résume les sept mesures et leur ordre de lecture. Les trois premières (éphémérité, isolation, moindre privilège) limitent ce qu'un attaquant obtient s'il exécute du code ; les trois suivantes (egress, secrets courts, vérification) limitent ce qu'il peut en faire ; la septième (immutabilité des entrées) réduit sa probabilité d'exécuter quoi que ce soit. Aucune n'est suffisante seule.
┌─────────────────────────────────────────────────────────────────────────────┐│ ARCHITECTURE DÉFENSIVE CI/CD │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 1. ÉPHÉMÉRITÉ 2. ISOLATION 3. LEAST PRIVILEGE ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ Runner détruit│ │ Pas de réseau│ │ Permissions │ ││ │ après chaque │ │ entre jobs │ │ minimales │ ││ │ job │ │ │ │ │ ││ └──────────────┘ └──────────────┘ └──────────────┘ ││ ││ 4. EGRESS CONTROL 5. SECRETS COURTS 6. VÉRIFICATION ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ Whitelist des │ │ OIDC, tokens │ │ Code review │ ││ │ destinations │ │ éphémères │ │ des workflows│ ││ │ réseau │ │ │ │ │ ││ └──────────────┘ └──────────────┘ └──────────────┘ ││ ││ 7. IMMUTABILITÉ DES INPUTS ││ ┌──────────────────────────────────────────────────────┐ ││ │ Actions pinées SHA, images digest, lockfiles figés │ ││ └──────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘Pilier 1 : Runners éphémères
Section intitulée « Pilier 1 : Runners éphémères »Principe : Chaque job s'exécute sur une machine vierge, détruite après exécution.
Le tableau compare les quatre modes que vous rencontrerez, sur les trois axes qui décident réellement du choix. Retenez surtout que la ligne runner persistant est le défaut de beaucoup d'installations self-hosted : c'est le mode où un job malveillant laisse derrière lui des credentials, un binaire modifié ou un cache empoisonné pour le job suivant.
| Mode | Sécurité | Performance | Coût |
|---|---|---|---|
| Runner persistant | ⚠️ Faible | ⭐⭐⭐ Rapide | 💰 Bas |
| Runner éphémère VM | ✅ Très bon isolement de base | ⭐⭐ Moyen | 💰💰 Moyen |
| Runner éphémère conteneur | ⚠️ Bon niveau possible, dépend de la config | ⭐⭐⭐ Rapide | 💰 Bas |
| GitHub-hosted | ✅ Élevée | ⭐⭐ Moyen | 💰💰💰 Élevé |
Pourquoi c'est crucial :
- Pas de persistance d'un attaquant entre les jobs
- Pas de credentials résiduels sur la machine
- Pas de malware persistant installé par un job précédent
Avec Actions Runner Controller (ARC), configurez des runners éphémères sur Kubernetes. Cet exemple est conceptuel, consultez la documentation ARC officielle pour le CRD exact selon votre version :
# Exemple conceptuel - vérifiez la doc ARC pour les runner scale setsapiVersion: actions.summerwind.dev/v1alpha1kind: RunnerDeploymentmetadata: name: ephemeral-runnersspec: replicas: 3 template: spec: ephemeral: true # Détruit après chaque job repository: votre-org/votre-repo labels: - ephemeral - linuxConfigurez des runners Docker avec hygiène renforcée (réduction du cache et de la persistance, sans être de vrais runners éphémères au sens infrastructurel) :
# config.toml - Runner Docker à faible persistance[[runners]] name = "docker-hygiene-renforcee" executor = "docker"
[runners.docker] image = "debian:bookworm-slim" privileged = false disable_cache = true # Pas de cache partagé pull_policy = "always"
# Nettoyage automatique cleanup_grace_period_seconds = 30Pilier 2 : Isolation entre jobs
Section intitulée « Pilier 2 : Isolation entre jobs »Principe : Un job ne doit pas pouvoir accéder aux ressources d'un autre job.
Il faut distinguer deux types d'isolation :
| Type | Description | Mécanismes |
|---|---|---|
| Isolation d'exécution | Séparation technique (processus, réseau, filesystem) | Runners éphémères, network policies, conteneurs/VMs dédiés |
| Séparation logique | Contrôle d'accès aux ressources (secrets, déploiements) | Environments, permissions, approbations |
# Aucune permission par défaut, chaque job rouvre le strict nécessairepermissions: {}
jobs: build: runs-on: ubuntu-latest # Isolation via environment environment: production
# Permissions minimales par job permissions: contents: read packages: write
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
# Pas d'accès aux secrets d'autres environments - name: Build env: API_KEY: ${{ secrets.PROD_API_KEY }} run: make buildbuild: stage: build # Isolation via environment environment: name: production
# Variables scoped à cet environment variables: API_KEY: $PROD_API_KEY
# Runner dédié avec tag tags: - production - isolated
script: - make buildPilier 3 : Least Privilege (permissions minimales)
Section intitulée « Pilier 3 : Least Privilege (permissions minimales) »Principe : Chaque job n'a accès qu'aux ressources strictement nécessaires.
En pratique, cela se joue sur un jeton unique, le GITHUB_TOKEN, injecté automatiquement dans chaque job. Ses droits par défaut dépendent des réglages du dépôt et de l'organisation, donc ils varient d'un projet à l'autre et changent au fil du temps. La seule position tenable est de les déclarer explicitement dans le workflow.
GitHub Actions : Permissions explicites
Section intitulée « GitHub Actions : Permissions explicites »Le bloc permissions: {} en tête de workflow retire tous les droits du GITHUB_TOKEN, y compris contents: read. Chaque job doit ensuite déclarer ce dont il a besoin, ce qui rend l'audit possible en lisant le seul fichier de workflow. Notez le persist-credentials: false sur actions/checkout : sans lui, l'action écrit le token dans .git/config, où n'importe quelle étape ultérieure peut le relire, y compris un script de build tiers.
# Désactiver les permissions par défautpermissions: {}
jobs: lint: runs-on: ubuntu-latest permissions: contents: read # Lecture du code uniquement steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: npm run lint
deploy: runs-on: ubuntu-latest needs: lint permissions: contents: read id-token: write # Pour OIDC packages: write # Pour publier steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Deploy run: make deployMatrice des permissions GitHub Actions
Section intitulée « Matrice des permissions GitHub Actions »Cette matrice sert d'aide à la revue : pour chaque permission demandée dans un workflow, vérifiez qu'un job la justifie vraiment. Deux lignes méritent une attention particulière, contents: write (qui permet de réécrire le dépôt, donc d'injecter du code dans un build futur) et id-token: write (qui ouvre la porte du cloud si la politique de confiance côté fournisseur est trop large).
| Permission | Quand l'utiliser | Risque si trop large |
|---|---|---|
contents: read | Checkout du code | Permet de lire le code (risque si donné à du code non fiable) |
contents: write | Push, merge, tags | Modification du dépôt, tags, releases, injection de code |
packages: write | Publication d'images/packages | Distribution de malware via registre |
id-token: write | OIDC vers cloud providers | Accès cloud si le trust côté provider est mal configuré |
actions: write | Gestion des workflows | Modification/annulation de workflows sensibles |
security-events: write | Upload de résultats de scan | Potentiel masquage de vulnérabilités |
Pilier 4 : Contrôle de l'egress (sortie réseau)
Section intitulée « Pilier 4 : Contrôle de l'egress (sortie réseau) »Principe : Limiter les destinations réseau autorisées pour empêcher l'exfiltration.
Pourquoi c'est critique :
- Un runner compromis peut envoyer des secrets vers un serveur C2
- Les attaques comme Codecov exfiltrent les données via HTTP
Implémentation avec Harden-Runner (GitHub Actions)
Section intitulée « Implémentation avec Harden-Runner (GitHub Actions) »Harden-Runner s'installe en première étape du job, avant checkout : il doit être actif avant que le moindre code tiers ne s'exécute. Commencez toujours en egress-policy: audit, qui n'interdit rien mais journalise chaque destination contactée. Sans ce passage d'observation, le mode blocage casse le build dès le premier miroir de paquets que vous aviez oublié.
jobs: build: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: # Mode audit (recommandé pour commencer) egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: npm ci - run: npm run buildAprès quelques runs, la liste des destinations légitimes est stable : passez en mode blocage en la reportant dans allowed-endpoints. Tout ce qui n'y figure pas est refusé, y compris les résolutions DNS.
- name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block allowed-endpoints: > github.com:443 registry.npmjs.org:443 nodejs.org:443Network policies Kubernetes (self-hosted)
Section intitulée « Network policies Kubernetes (self-hosted) »Cet exemple montre une restriction de port (443 uniquement), pas une vraie whitelist de destinations :
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: runner-egress-port-restrictionspec: podSelector: matchLabels: app: github-runner policyTypes: - Egress egress: # Restriction de PORT, pas de destination - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 443 protocol: TCP # Ceci autorise tout Internet en 443, pas une whitelistPilier 5 : Secrets courte durée (OIDC)
Section intitulée « Pilier 5 : Secrets courte durée (OIDC) »Principe : Remplacer les secrets statiques par des tokens éphémères via OIDC.
Un secret statique stocké dans GitHub ou GitLab reste valide tant que personne ne le révoque, c'est-à-dire en pratique des mois après une fuite. OIDC supprime purement le secret du pipeline : le job présente une identité vérifiable et reçoit en échange des credentials qui expirent en quelques minutes.
Le problème des secrets statiques
Section intitulée « Le problème des secrets statiques »Ces quatre secrets se retrouvent dans presque tous les pipelines. Leur point commun est d'être exfiltrables en une ligne (un echo mal placé, un log verbeux, une dépendance malveillante) et de rester exploitables longtemps après la fuite, sans que rien ne le signale.
| Secret statique | Risque |
|---|---|
| AWS Access Key | Valide indéfiniment, exfiltrable |
| Docker Hub token | Permet de publier des images |
| NPM token | Permet de publier des packages |
| Kubeconfig | Accès complet au cluster |
La solution : OIDC (OpenID Connect)
Section intitulée « La solution : OIDC (OpenID Connect) »L'échange se déroule en trois temps, détaillés sous le schéma. Le point à comprendre est que rien n'est stocké côté pipeline : le jeton JWT est fabriqué à la demande par la forge, il porte l'identité du workflow (dépôt, branche, environnement) et il n'a de valeur que pour le fournisseur cloud qui l'a explicitement autorisé.
┌────────────────┐ ┌────────────────┐ ┌────────────────┐│ GitHub Actions │────────▶│ AWS / GCP │────────▶│ Ressource ││ │ Token │ IAM / WIF │ Accès │ Cloud ││ │ OIDC │ │ temp. │ │└────────────────┘ └────────────────┘ └────────────────┘
1. GitHub génère un token OIDC (JWT) avec l'identité du workflow2. Le cloud provider vérifie le token et émet des credentials temporaires3. Les credentials expirent après quelques minutes/heuresjobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::123456789:role/github-actions aws-region: eu-west-1 # Pas de secrets ! Le token OIDC suffit
- name: Deploy to S3 run: aws s3 sync ./dist s3://my-bucketjobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Authenticate to Google Cloud (OIDC) uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: workload_identity_provider: projects/123/locations/global/workloadIdentityPools/my-pool/providers/github service_account: deploy@my-project.iam.gserviceaccount.com
- name: Deploy run: gcloud run deploy my-service --image=...jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Azure Login (OIDC) uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Pas de client secret, OIDC est utilisé
- name: Deploy run: az webapp deploy ...Pilier 6 : Vérification des inputs
Section intitulée « Pilier 6 : Vérification des inputs »Principe : Valider et restreindre ce qui peut déclencher et modifier le pipeline.
Le code d'une pull request est du code non approuvé, même quand il vient d'un collègue. Deux questions décident du niveau de risque : quel événement déclenche le workflow, et quel ref est réellement extrait par checkout.
Protections sur les pull requests
Section intitulée « Protections sur les pull requests »Le workflow ci-dessous refuse d'exécuter le job quand la PR vient d'un fork (head.repo.full_name diffère du dépôt), et extrait la branche de base plutôt que le contenu de la PR. Réservez ce schéma aux workflows sensibles : pour la CI ordinaire, tester le code de la PR est justement le but, mais alors sans permissions ni secrets.
on: pull_request: # Ne pas exécuter sur les PRs des forks (par défaut) # Utiliser pull_request_target avec précaution
jobs: build: runs-on: ubuntu-latest # Vérifier que le workflow n'a pas été modifié dans la PR if: github.event.pull_request.head.repo.full_name == github.repository
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Checkout la base, pas la PR (pour les workflows sensibles) ref: ${{ github.base_ref }}Bloquer les modifications de fichiers sensibles
Section intitulée « Bloquer les modifications de fichiers sensibles »Le fetch-depth: 0 est indispensable ici : sans l'historique complet, le git diff contre origin/main échoue faute de base de comparaison. Le job s'arrête en erreur dès qu'une PR touche un workflow, le Dockerfile ou le Makefile, c'est-à-dire les fichiers qui décident de ce que la CI exécute.
jobs: check-files: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0
- name: Check for sensitive file changes run: | SENSITIVE_FILES=".github/workflows/ Dockerfile Makefile" CHANGED=$(git diff --name-only origin/main...HEAD)
for file in $SENSITIVE_FILES; do if echo "$CHANGED" | grep -q "$file"; then echo "::error::Modification of $file requires special approval" exit 1 fi donePilier 7 : Immutabilité des inputs
Section intitulée « Pilier 7 : Immutabilité des inputs »Principe : Figer les entrées de build pour éviter les modifications silencieuses.
Un tag Git est mutable : v4 peut être redéplacé sur un autre commit sans que rien ne change dans votre dépôt. C'est exactement le mécanisme exploité lors de l'incident tj-actions en 2025. Le tableau liste les quatre familles d'entrées et la seule forme de référence immuable pour chacune.
| Input | Risque si non figé | Bonne pratique |
|---|---|---|
| Actions GitHub | Tag mutable peut être modifié | Pinner par SHA |
| Images Docker | Tag latest ou version peut changer | Pinner par digest |
| Dépendances | Résolution dynamique peut changer | Lockfiles obligatoires |
| Scripts téléchargés | Contenu peut être modifié | Interdire curl | bash |
Écrivez systématiquement le tag en commentaire derrière le SHA : sans lui, personne ne sait quelle version tourne et les mises à jour ne se font plus.
# Actions pinées par SHA- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Images pinées par digestFROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710deLe digest ci-dessus est celui de python:3.12-slim au 23 juillet 2026, relevé avec docker inspect --format '{{index .RepoDigests 0}}' python:3.12-slim. Relevez le vôtre de la même façon plutôt que de recopier celui-ci : un digest désigne une image précise et figée, c'est tout l'intérêt, mais cela signifie aussi qu'il ne recevra plus jamais de correctif. Épingler par digest impose donc de rafraîchir volontairement, sinon vous figez aussi les vulnérabilités.
Un script distribué en HTTP n'a aucune de ces garanties. Si un outil ne s'installe que par téléchargement direct, récupérez l'archive et le fichier de sommes publiés par l'éditeur, vérifiez, puis seulement extrayez :
curl -fsSLO https://exemple-editeur.tld/outil-1.2.3-linux-amd64.tar.gzcurl -fsSLO https://exemple-editeur.tld/outil-1.2.3-checksums.txtsha256sum --check --ignore-missing outil-1.2.3-checksums.txttar -xzf outil-1.2.3-linux-amd64.tar.gzLes URL ci-dessus sont des exemples de forme : reprenez celles publiées par l'éditeur de votre outil. Le point qui compte est l'ordre, sha256sum --check doit réussir avant que la moindre ligne du contenu téléchargé ne soit exécutée ou extraite.
Hardening pas à pas
Section intitulée « Hardening pas à pas »Les deux checklists qui suivent traduisent les sept piliers en gestes concrets, une par plateforme. Elles sont ordonnées du plus rentable au plus coûteux : les trois premiers points de chaque liste se posent en une demi-journée et couvrent déjà les vecteurs les plus exploités. Traitez-les dans l'ordre plutôt qu'en parallèle, chaque étape se vérifie sur un pipeline réel avant de passer à la suivante.
Checklist de hardening GitHub Actions
Section intitulée « Checklist de hardening GitHub Actions »Appliquez ces sept points sur un workflow existant, pas sur un fichier de démonstration : c'est en le faisant sur du code qui tourne que vous découvrez les dépendances implicites (un job qui écrivait un tag, une action qui lisait le token du .git/config).
-
Permissions minimales globales
Coupez tout au niveau du workflow avec
permissions: {}, puis rouvrez job par job. Uncontents: readglobal paraît anodin, mais il donne l'accès au code à chaque job, y compris ceux qui exécutent des dépendances tierces.# Au niveau du workflow : aucune permission par défautpermissions: {}jobs:lint:permissions:contents: read -
Pinning des actions par SHA
# ❌ Mauvais- uses: actions/checkout@v4# ✅ Bon- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Utilisez pin-github-action pour automatiser.
-
OIDC pour tous les déploiements cloud
Remplacez les secrets statiques AWS/GCP/Azure par OIDC.
-
Harden-Runner pour le contrôle egress
- uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0with:egress-policy: audit # puis block -
Environments pour les secrets sensibles
Configurez des environments avec approbation pour la production.
-
Code review obligatoire pour les workflows
Ajoutez les fichiers
.github/workflows/**dans les CODEOWNERS. -
Audit régulier des permissions
Utilisez StepSecurity Secure Repo pour auditer.
Checklist de hardening GitLab CI
Section intitulée « Checklist de hardening GitLab CI »GitLab répartit les réglages différemment : une grande partie du durcissement ne vit pas dans le .gitlab-ci.yml mais dans les paramètres du projet (variables protégées, branches protégées, approbations). Les six points ci-dessous indiquent à chaque fois de quel côté se fait le réglage.
-
Variables protégées et masquées (côté GitLab UI)
Les variables protégées et masquées se configurent dans GitLab (Settings → CI/CD → Variables), pas dans le YAML :
- Protected : uniquement disponibles sur branches/tags protégés
- Masked : masquées dans les logs de pipeline
Dans le
.gitlab-ci.yml, vous accédez simplement à la variable :deploy_prod:script:- echo "Deploying with key..."- deploy --api-key=$PROD_API_KEY # Configuré côté GitLabrules:- if: $CI_COMMIT_REF_PROTECTED == "true" -
Runners dédiés par environment
deploy_prod:tags:- production- secureenvironment:name: production -
Review apps isolées
review:environment:name: review/$CI_COMMIT_REF_SLUGauto_stop_in: 1 week -
Protected branches + merge rules
- Exiger 2 approbations pour
main - Pipeline réussi obligatoire
- Pas de push direct
- Exiger 2 approbations pour
-
Secrets via OIDC + Vault (recommandé)
Utilisez les ID tokens OIDC pour authentification sans secrets statiques :
deploy_prod:id_tokens:VAULT_ID_TOKEN:aud: https://vault.example.comsecrets:DATABASE_PASSWORD:vault:engine:name: kv-v2path: productionpath: dbfield: passwordtoken: $VAULT_ID_TOKENscript:- deploy --db-password=$DATABASE_PASSWORDLe job reçoit un token OIDC que Vault vérifie avant d'émettre le secret.
-
Audit des jobs avec
needsdeploy:needs:- test- security_scanwhen: on_success # Uniquement si les jobs précédents réussissent
Architecture cible alignée SLSA
Section intitulée « Architecture cible alignée SLSA »Une fois les sept piliers en place, la question suivante est celle de la preuve : comment démontrer à un tiers que l'artefact publié vient bien de ce build. C'est le domaine du référentiel SLSA, et c'est là que le hardening cesse d'être une hygiène interne pour devenir un engagement vérifiable. La section pose la cible puis relie chaque pilier au niveau SLSA qu'il aide à atteindre.
Vue d'ensemble
Section intitulée « Vue d'ensemble »Cette architecture représente une cible d'inspiration SLSA niveau 3, pas une conformité automatique. Les exigences SLSA sont précises et dépendent des détails d'implémentation.
┌─────────────────────────────────────────────────────────────────────────────┐│ ARCHITECTURE CIBLE POUR BUILDS DURCIS (type SLSA L3) │├─────────────────────────────────────────────────────────────────────────────┤│ ││ SOURCE BUILDER ARTEFACTS ││ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ │ │ │ │ │ ││ │ Git + │────────▶│ Runner │──────▶│ Image + │ ││ │ Signed │ │ Éphémère │ │ SBOM + │ ││ │ Commits │ │ │ │ Provenance │ ││ │ │ │ Hermetic │ │ Signée │ ││ └──────────┘ │ Build │ └──────────────┘ ││ │ │ │ │ ││ │ └──────────────┘ │ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ Signature│ │ Pas d'accès │ │ Vérification │ ││ │ GPG/SSH │ │ réseau sauf │ │ avant │ ││ │ │ │ whitelist │ │ déploiement │ ││ └──────────┘ └──────────────┘ └──────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘Mapping SLSA ↔ piliers de hardening
Section intitulée « Mapping SLSA ↔ piliers de hardening »Ce tableau est une lecture opérationnelle simplifiée pour relier le hardening du build à la progression SLSA ; il ne remplace pas la lecture des exigences officielles SLSA.
| Niveau SLSA | Exigences | Piliers couverts |
|---|---|---|
| Level 1 | Provenance basique | Éphémérité (partiel), Immutabilité |
| Level 2 | Provenance signée, build service | Éphémérité, Isolation, Vérification, Immutabilité |
| Level 3 | Isolation complète, provenance non-falsifiable | Tous les piliers |
Monitoring et détection
Section intitulée « Monitoring et détection »Le durcissement réduit la surface d'attaque, il ne prévient pas de l'échec. Un pipeline compromis se manifeste par des écarts de comportement mesurables : durée anormale, destinations réseau nouvelles, modification hors du circuit de revue. Encore faut-il collecter ces signaux et savoir lesquels méritent une alerte, ce que détaillent les deux tableaux suivants.
Signaux d'alerte à surveiller
Section intitulée « Signaux d'alerte à surveiller »Ces cinq signaux sont observables sans outillage spécifique, à partir des données que la forge produit déjà. Le premier, la durée anormale d'un job, est souvent le plus précoce : chiffrer, miner ou exfiltrer coûte du temps CPU, et cela se voit sur la moyenne historique du job.
| Signal | Ce qu'il indique | Action |
|---|---|---|
| Job anormalement long | Exfiltration, crypto mining | Alerter + investiguer |
| Connexions réseau inattendues | C2, exfiltration | Bloquer + alerter |
| Modification de workflow sans PR | Compromission | Bloquer + révoquer |
| Échecs de signature répétés | Tentative de contournement | Alerter |
| Nouveau mainteneur sur action utilisée | Supply chain risk | Revoir l'action |
Outils de monitoring
Section intitulée « Outils de monitoring »Aucun de ces outils ne couvre tout le spectre, ils se répartissent par couche : le réseau du runner, les dépendances, les actions sur le dépôt, le runtime du conteneur. Le GitHub Audit Log est le seul disponible sans installation, et c'est celui qui répond à la question « qui a modifié ce workflow, et quand ».
| Outil | Fonction |
|---|---|
| Harden-Runner | Log des connexions réseau depuis les runners |
| Dependency Review | Alertes sur nouvelles dépendances vulnérables |
| GitHub Audit Log | Toutes les actions sur le repo/org |
| Falco | Runtime security pour runners Kubernetes |
| Datadog CI Visibility | Métriques et traces des pipelines |
Ce que ce guide ne couvre pas
Section intitulée « Ce que ce guide ne couvre pas »Le hardening du build est une pièce essentielle, mais pas suffisante seule :
| Sujet | Où le traiter |
|---|---|
| Sécurité du code source | Revue de code, SAST, policies de contribution |
| Sécurité des dépendances | SBOM, SCA, veille CVE, lockfiles |
| Signature et provenance des artefacts | Sigstore, SLSA |
| Admission policy côté cluster | Kyverno, OPA Gatekeeper, vérification provenance |
| Gestion des mainteneurs tiers | Due diligence, audit des actions/packages utilisés |
Priorisation réaliste
Section intitulée « Priorisation réaliste »Si vous partez de zéro, voici l'ordre recommandé :
-
Permissions minimales, impact immédiat, facile à implémenter
-
Pinning des actions et images, protège contre les modifications silencieuses
-
OIDC pour les déploiements cloud, élimine les secrets statiques
-
Runners éphémères, élimine la persistance
-
Audit egress (mode observation), comprendre le trafic réseau
-
Blocage egress, après avoir validé les destinations légitimes
-
Architecture hermétique complète, objectif long terme
Compromis coût / sécurité / opérabilité
Section intitulée « Compromis coût / sécurité / opérabilité »Le hardening a un coût. Assumez ces compromis plutôt que de les subir :
| Mesure | Bénéfice sécurité | Coût / friction |
|---|---|---|
| Runners VM éphémères | Isolation maximale | Plus cher, plus lent que conteneurs |
| Egress strict | Bloque exfiltration | Nécessite maintenance de la whitelist |
| OIDC | Plus de secrets statiques | Paramétrage IAM initial |
| Revue obligatoire des workflows | Bloque les modifications malveillantes | Ralentit légèrement le développement |
| Pinning par SHA | Prévient les modifications furtives | Maintenance des mises à jour |
À retenir
Section intitulée « À retenir »Les 6 points essentiels de ce guide :
- Le pipeline CI/CD est une cible critique, il dispose d'accès privilégiés aux ressources sensibles
- 7 piliers de défense : éphémérité, isolation, least privilege, egress control, secrets courts (OIDC), vérification des inputs, immutabilité des inputs
- Les runners éphémères sont la base, mais un conteneur n'est pas automatiquement aussi isolé qu'une VM
- OIDC remplace les secrets statiques, mais le niveau de sécurité dépend du trust configuré côté cloud
- Le hardening est un prérequis pratique pour progresser vers SLSA, pas une garantie automatique de conformité
- Priorisez : permissions, pinning, OIDC d'abord ; egress strict et architecture hermétique ensuite
Prochaines étapes
Section intitulée « Prochaines étapes »SLSA
Comprenez les niveaux SLSA et comment les atteindre Lire le guide SLSA
Attaques GitHub Actions
Techniques offensives et défensives sur les pipelines Lire le guide
Sigstore
Signez vos artefacts avec Cosign et Fulcio Lire le guide Sigstore
SBOM
Générez un inventaire de vos composants à chaque build Lire le guide SBOM