Aller au contenu
Sécurité medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

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

29 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

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

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
jobs:
build:
runs-on: ubuntu-latest
# Isolation via environment
environment: production
# Permissions minimales par job
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
# 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.

# 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@v4
- 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@v4
- name: Deploy
run: make deploy
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

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) »
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2
with:
# Mode audit (recommandé pour commencer)
egress-policy: audit
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build

Après quelques runs, passez en mode blocage :

- name: Harden Runner
uses: step-security/harden-runner@v2
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.

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
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 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@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
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.

.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@v4
with:
# Checkout la base, pas la PR (pour les workflows sensibles)
ref: ${{ github.base_ref }}
.github/workflows/ci.yml
jobs:
check-files:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
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.

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
# Actions pinées par SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Images pinées par digest
FROM python:3.12-slim@sha256:abc123...
  1. Permissions minimales globales

    # Au niveau du workflow
    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@v2
    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.

  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

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
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
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 tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn