Aller au contenu
Sécurité medium

Hardening de l'environnement de build : sécuriser runners et pipelines

40 min de lecture

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.

  • 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

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.

L'environnement de build est un actif critique car il :

CaractéristiqueRisque associé
A accès aux secrets de productionExfiltration de credentials
Peut modifier le code (merge, tag)Injection de backdoors
Signe les artefactsSignature d'artefacts compromis
Publie vers les registresDistribution 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.

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 │ │
│ └───────────────────┘ └───────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

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.

AttaqueVecteurImpact
Codecov (2021)Script bash modifié dans l'image de buildExfiltration de secrets CI vers l'attaquant
Event-stream (2018)Mainteneur compromis → code malveillantVol de Bitcoin via dépendance npm
SolarWinds (2020)Compromission du build systemBackdoor dans 18 000 organisations
tj-actions (2025)Action GitHub compromiseExfiltration de secrets
xz-utils (2024)Build scripts modifiésBackdoor SSH dans binaires

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.

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 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

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.

ModeSécuritéPerformanceCoû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 sets
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: ephemeral-runners
spec:
replicas: 3
template:
spec:
ephemeral: true # Détruit après chaque job
repository: votre-org/votre-repo
labels:
- ephemeral
- linux

Principe : Un job ne doit pas pouvoir accéder aux ressources d'un autre job.

Il faut distinguer deux types d'isolation :

TypeDescriptionMécanismes
Isolation d'exécutionSéparation technique (processus, réseau, filesystem)Runners éphémères, network policies, conteneurs/VMs dédiés
Séparation logiqueContrôle d'accès aux ressources (secrets, déploiements)Environments, permissions, approbations
.github/workflows/secure-build.yml
# Aucune permission par défaut, chaque job rouvre le strict nécessaire
permissions: {}
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 build

Pilier 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.

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éfaut
permissions: {}
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 deploy

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).

PermissionQuand l'utiliserRisque si trop large
contents: readCheckout du codePermet de lire le code (risque si donné à du code non fiable)
contents: writePush, merge, tagsModification du dépôt, tags, releases, injection de code
packages: writePublication d'images/packagesDistribution de malware via registre
id-token: writeOIDC vers cloud providersAccès cloud si le trust côté provider est mal configuré
actions: writeGestion des workflowsModification/annulation de workflows sensibles
security-events: writeUpload de résultats de scanPotentiel masquage de vulnérabilités

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 build

Aprè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:443

Cet exemple montre une restriction de port (443 uniquement), pas une vraie whitelist de destinations :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress-port-restriction
spec:
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 whitelist

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.

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 statiqueRisque
AWS Access KeyValide indéfiniment, exfiltrable
Docker Hub tokenPermet de publier des images
NPM tokenPermet de publier des packages
KubeconfigAccès complet au cluster

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 workflow
2. Le cloud provider vérifie le token et émet des credentials temporaires
3. Les credentials expirent après quelques minutes/heures
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: 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-bucket

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.

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.

.github/workflows/ci.yml
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 }}

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.

.github/workflows/ci.yml
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
done

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.

InputRisque si non figéBonne pratique
Actions GitHubTag mutable peut être modifiéPinner par SHA
Images DockerTag latest ou version peut changerPinner par digest
DépendancesRésolution dynamique peut changerLockfiles obligatoires
Scripts téléchargésContenu 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 digest
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de

Le 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 :

Fenêtre de terminal
curl -fsSLO https://exemple-editeur.tld/outil-1.2.3-linux-amd64.tar.gz
curl -fsSLO https://exemple-editeur.tld/outil-1.2.3-checksums.txt
sha256sum --check --ignore-missing outil-1.2.3-checksums.txt
tar -xzf outil-1.2.3-linux-amd64.tar.gz

Les 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.

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.

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).

  1. Permissions minimales globales

    Coupez tout au niveau du workflow avec permissions: {}, puis rouvrez job par job. Un contents: read global 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éfaut
    permissions: {}
    jobs:
    lint:
    permissions:
    contents: read
  2. Pinning des actions par SHA

    # ❌ Mauvais
    - uses: actions/checkout@v4
    # ✅ Bon
    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    Utilisez pin-github-action pour automatiser.

  3. OIDC pour tous les déploiements cloud

    Remplacez les secrets statiques AWS/GCP/Azure par OIDC.

  4. Harden-Runner pour le contrôle egress

    - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
    with:
    egress-policy: audit # puis block
  5. Environments pour les secrets sensibles

    Configurez des environments avec approbation pour la production.

  6. Code review obligatoire pour les workflows

    Ajoutez les fichiers .github/workflows/** dans les CODEOWNERS.

  7. Audit régulier des permissions

    Utilisez StepSecurity Secure Repo pour auditer.

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.

  1. 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é GitLab
    rules:
    - if: $CI_COMMIT_REF_PROTECTED == "true"
  2. Runners dédiés par environment

    deploy_prod:
    tags:
    - production
    - secure
    environment:
    name: production
  3. Review apps isolées

    review:
    environment:
    name: review/$CI_COMMIT_REF_SLUG
    auto_stop_in: 1 week
  4. Protected branches + merge rules

    • Exiger 2 approbations pour main
    • Pipeline réussi obligatoire
    • Pas de push direct
  5. 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.com
    secrets:
    DATABASE_PASSWORD:
    vault:
    engine:
    name: kv-v2
    path: production
    path: db
    field: password
    token: $VAULT_ID_TOKEN
    script:
    - deploy --db-password=$DATABASE_PASSWORD

    Le job reçoit un token OIDC que Vault vérifie avant d'émettre le secret.

  6. Audit des jobs avec needs

    deploy:
    needs:
    - test
    - security_scan
    when: on_success # Uniquement si les jobs précédents réussissent

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.

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 │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

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 SLSAExigencesPiliers couverts
Level 1Provenance basiqueÉphémérité (partiel), Immutabilité
Level 2Provenance signée, build serviceÉphémérité, Isolation, Vérification, Immutabilité
Level 3Isolation complète, provenance non-falsifiableTous les piliers

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.

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.

SignalCe qu'il indiqueAction
Job anormalement longExfiltration, crypto miningAlerter + investiguer
Connexions réseau inattenduesC2, exfiltrationBloquer + alerter
Modification de workflow sans PRCompromissionBloquer + révoquer
Échecs de signature répétésTentative de contournementAlerter
Nouveau mainteneur sur action utiliséeSupply chain riskRevoir l'action

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 ».

OutilFonction
Harden-RunnerLog des connexions réseau depuis les runners
Dependency ReviewAlertes sur nouvelles dépendances vulnérables
GitHub Audit LogToutes les actions sur le repo/org
FalcoRuntime security pour runners Kubernetes
Datadog CI VisibilityMétriques et traces des pipelines

Le hardening du build est une pièce essentielle, mais pas suffisante seule :

SujetOù le traiter
Sécurité du code sourceRevue de code, SAST, policies de contribution
Sécurité des dépendancesSBOM, SCA, veille CVE, lockfiles
Signature et provenance des artefactsSigstore, SLSA
Admission policy côté clusterKyverno, OPA Gatekeeper, vérification provenance
Gestion des mainteneurs tiersDue diligence, audit des actions/packages utilisés

Si vous partez de zéro, voici l'ordre recommandé :

  1. Permissions minimales, impact immédiat, facile à implémenter

  2. Pinning des actions et images, protège contre les modifications silencieuses

  3. OIDC pour les déploiements cloud, élimine les secrets statiques

  4. Runners éphémères, élimine la persistance

  5. Audit egress (mode observation), comprendre le trafic réseau

  6. Blocage egress, après avoir validé les destinations légitimes

  7. Architecture hermétique complète, objectif long terme

Le hardening a un coût. Assumez ces compromis plutôt que de les subir :

MesureBénéfice sécuritéCoût / friction
Runners VM éphémèresIsolation maximalePlus cher, plus lent que conteneurs
Egress strictBloque exfiltrationNécessite maintenance de la whitelist
OIDCPlus de secrets statiquesParamétrage IAM initial
Revue obligatoire des workflowsBloque les modifications malveillantesRalentit légèrement le développement
Pinning par SHAPrévient les modifications furtivesMaintenance des mises à jour

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

Attaques GitHub Actions

Techniques offensives et défensives sur les pipelines Lire le guide

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