Aller au contenu
CI/CD & Automatisation medium

Isolation des runners GitHub Actions

17 min de lecture

L'isolation des runners permet de contrôler quels workflows peuvent s'exécuter sur quelles machines. C'est essentiel pour la sécurité et la gestion des ressources dans les grandes organisations.

  • Comprendre pourquoi isoler les runners (sécurité, performance, conformité)
  • Router les jobs avec les labels et les groupes de runners
  • Appliquer les patterns d'isolation : par environnement, confiance, équipe
  • Contrôler l'accès au niveau dépôt ou organisation
  • Segmenter le réseau pour cloisonner les runners

Sans isolation, un parc de runners self-hosted forme un pool unique : tout job du dépôt ou de l'organisation peut atterrir sur n'importe quelle machine. Trois catégories de problèmes en découlent, et elles ne se traitent pas au même niveau : la première relève de la sécurité, la deuxième de la capacité, la troisième de la traçabilité.

Une machine qui exécute à la fois un job de PR et un déploiement de production donne au premier l'accès aux ressources du second : réseau interne, jetons en cache, images construites. Séparer les runners, c'est décider à l'avance ce qu'un job de confiance moyenne est capable d'atteindre.

  • Empêcher les PRs non fiables d'accéder au réseau de production
  • Limiter l'impact d'un workflow compromis
  • Respecter le principe de moindre privilège

Les jobs se répartissent sur les runners qui portent les labels demandés, sans notion de priorité : une file de tests d'une équipe peut donc bloquer le déploiement d'une autre. Dédier des machines par usage garantit une capacité réservée à ce qui ne peut pas attendre.

  • Réserver des runners puissants aux builds critiques
  • Éviter la contention entre équipes
  • Garantir des SLAs différents par projet

Certaines contraintes ne se satisfont pas d'une bonne intention : données de production qui ne doivent jamais transiter par une machine de développement, traitements qui doivent rester dans une zone géographique donnée. Le rattachement d'un job à une machine identifiée est ce qui rend la démonstration possible lors d'un audit.

  • Séparer les données sensibles par environnement
  • Traçabilité des exécutions par équipe
  • Respect des zones géographiques

GitHub offre deux leviers complémentaires, qu'il ne faut pas confondre. Les labels font du routage : ils expriment ce qu'un job demande et déterminent la machine qui l'exécutera. Les groupes de runners font du contrôle d'accès : ils déterminent quels dépôts ont le droit d'utiliser ces machines. Un label seul n'interdit rien, n'importe quel workflow du dépôt peut l'écrire dans son runs-on.

Les labels identifient les capacités d'un runner : un job n'est distribué qu'aux machines portant tous ceux qu'il réclame. Ils se déclarent à l'enregistrement, mais restent modifiables ensuite depuis les réglages du dépôt ou de l'organisation.

Fenêtre de terminal
# À l'enregistrement
./config.sh --url https://github.com/ORG/REPO \
--token TOKEN \
--labels linux,x64,docker,gpu,production

Utilisation dans un workflow :

jobs:
build:
runs-on: [self-hosted, linux, docker]
deploy-prod:
runs-on: [self-hosted, production]
ml-training:
runs-on: [self-hosted, gpu]

Au niveau organisation, les groupes permettent de limiter l'accès : un runner appartient à exactement un groupe, et le groupe énumère les dépôts autorisés. C'est le seul mécanisme qui empêche réellement un dépôt d'envoyer un job vers une machine, là où les labels se contentent de l'aiguiller.

  1. Allez dans Settings > Actions > Runner groups
  2. Créez un groupe (ex: production-runners)
  3. Assignez des runners au groupe
  4. Définissez quels repos peuvent l'utiliser
# Seuls les repos autorisés peuvent utiliser ce runner
jobs:
deploy:
runs-on:
group: production-runners
labels: [linux, x64]

Trois découpages couvrent la plupart des besoins. Ils se combinent : un runner peut être à la fois production, team-platform et gpu.

Le découpage le plus courant fait correspondre un jeu de runners à chaque étage de la chaîne de livraison. L'intérêt n'est pas le label en lui-même mais ce qu'il implique derrière : chaque groupe de machines vit dans le réseau de son environnement et n'a d'accès qu'aux bases et API correspondantes. La clé environment: ajoute une seconde barrière, côté GitHub cette fois, avec ses règles d'approbation et ses secrets propres.

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Dev Runners │ │ Staging Runners │ │ Prod Runners │
│ │ │ │ │ │
│ Labels: │ │ Labels: │ │ Labels: │
│ - dev │ │ - staging │ │ - production │
│ - self-hosted │ │ - self-hosted │ │ - self-hosted │
│ │ │ │ │ │
│ Accès: tous │ │ Accès: équipe QA │ │ Accès: ops │
└─────────────────┘ └──────────────────┘ └─────────────────┘
jobs:
test:
runs-on: [self-hosted, dev]
staging:
runs-on: [self-hosted, staging]
environment: staging # Protection supplémentaire
production:
runs-on: [self-hosted, production]
environment: production

Ce découpage classe les jobs selon l'origine du code exécuté : un push sur main a été relu et fusionné, une PR ne l'a pas encore été. Les runners untrusted n'ont alors qu'un accès Internet sortant, sans route vers le réseau interne ni secrets de production. L'expression ternaire dans runs-on fait le choix au démarrage du job, à partir de github.event_name.

┌─────────────────────┐ ┌─────────────────────┐
│ Trusted Runners │ │ Untrusted Runners │
│ │ │ │
│ Pour: │ │ Pour: │
│ - Pushes sur main │ │ - PRs externes │
│ - Tags de release │ │ - Forks │
│ │ │ │
│ Accès: │ │ Accès: │
│ - Réseau interne │ │ - Internet seul │
│ - Secrets sensibles │ │ - Secrets limités │
└─────────────────────┘ └─────────────────────┘
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
# Code de confiance (push) → runner trusted ; PR → runner untrusted
runs-on:
- self-hosted
- ${{ github.event_name == 'push' && 'trusted' || 'untrusted' }}

Ce troisième axe répond à un besoin de capacité et de refacturation plutôt que de sécurité : chaque équipe dispose de ses machines et ne subit pas la file d'attente des autres. Les labels se cumulent avec ceux des axes précédents, un job pouvant réclamer à la fois son équipe et un matériel particulier.

jobs:
frontend-build:
runs-on: [self-hosted, team-frontend]
backend-build:
runs-on: [self-hosted, team-backend]
ml-training:
runs-on: [self-hosted, team-data, gpu]

Le niveau d'enregistrement d'un runner détermine qui peut l'utiliser. Plus le périmètre est large, plus le contrôle d'accès doit être strict.

Un runner enregistré au niveau repository n'apparaît que dans ce dépôt : aucun autre projet ne peut lui envoyer de job, même en devinant ses labels. Au prix d'une machine sous-utilisée et à maintenir pour un seul projet, ce runner :

  • N'est accessible que par ce repository
  • Isolation maximale
Fenêtre de terminal
./config.sh --url https://github.com/OWNER/REPO ...

Un runner enregistré au niveau organisation est mutualisé, ce qui améliore son taux d'occupation mais élargit d'autant sa surface d'exposition. Le groupe n'y est plus optionnel, il redevient la frontière qui décide quels dépôts peuvent atteindre la machine. Un tel runner :

  • Peut être partagé entre repos
  • Contrôlé par les groupes
Fenêtre de terminal
./config.sh --url https://github.com/ORG ...

Utilisez les environments pour ajouter une couche de contrôle : un environnement GitHub porte ses propres secrets, ses règles de protection et, si vous l'activez, une approbation manuelle avant démarrage. Le job reste en attente tant que l'approbation n'est pas donnée, et les secrets ne sont transmis au runner qu'à ce moment-là.

jobs:
deploy:
runs-on: [self-hosted, production]
environment:
name: production
# Nécessite approbation manuelle

L'isolation par labels et par groupes se joue côté GitHub : elle décide où part un job. Elle ne dit rien de ce que la machine peut joindre une fois le job lancé. Un runner mal placé dans le réseau reste joignable depuis le job, quelle que soit la propreté des labels. Le cloisonnement réseau est donc la couche qui rend l'isolation logique effective.

Chaque environnement occupe son propre sous-réseau, et un runner ne voit que les services du sien. Le job de dev qui tenterait d'atteindre la base de production échoue au niveau réseau, sans dépendre d'une configuration correcte du workflow.

┌─────────────────────────────────────────────────────────────┐
│ VPC / Réseau │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Subnet Dev │ Subnet Staging │ Subnet Production │
│ │ │ │
│ ┌─────────────┐ │ ┌─────────────┐ │ ┌─────────────────────┐ │
│ │ Dev Runner │ │ │Staging Runner│ │ │ Prod Runner │ │
│ │ │ │ │ │ │ │ │ │
│ │ Accès: │ │ │ Accès: │ │ │ Accès: │ │
│ │ - DB dev │ │ │ - DB staging│ │ │ - DB prod │ │
│ │ - API dev │ │ │ - API staging│ │ │ - K8s prod │ │
│ └─────────────┘ │ └─────────────┘ │ └─────────────────────┘ │
└─────────────────┴─────────────────┴─────────────────────────┘

Les règles se construisent en liste blanche : on autorise le sous-réseau utile et le port 443 vers GitHub, puis on jette le reste. Le DROP final est ce qui limite l'exfiltration en cas de job compromis. Vérifiez que les miroirs de paquets et les registres d'images restent joignables, faute de quoi les premiers builds échoueront sur des délais d'attente réseau.

Fenêtre de terminal
# Runner dev : accès limité
iptables -A OUTPUT -d 10.0.1.0/24 -j ACCEPT # Subnet dev
iptables -A OUTPUT -d 0.0.0.0/0 -p tcp --dport 443 -j ACCEPT # GitHub
iptables -A OUTPUT -j DROP
# Runner production : accès plus large mais contrôlé
iptables -A OUTPUT -d 10.0.0.0/16 -j ACCEPT # Tout le VPC
iptables -A OUTPUT -d 0.0.0.0/0 -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -j DROP

Ce workflow met bout à bout les patterns du guide : tests sur runners mutualisés, build sur runners d'équipe, déploiements sur runners par environnement avec approbation pour la production.

.github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
# Aucun droit par défaut : chaque job demande le minimum
permissions: {}
jobs:
# Tests sur runners mutualisés
test:
runs-on: [self-hosted, linux, shared]
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm test
# Build sur runners de l'équipe
build:
needs: test
runs-on: [self-hosted, linux, team-platform]
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: docker build -t app .
# Déploiement staging (réseau staging)
deploy-staging:
if: github.ref == 'refs/heads/develop'
needs: build
runs-on: [self-hosted, staging-network]
environment: staging
steps:
- run: kubectl --context staging apply -f k8s/
# Déploiement production (réseau prod, approbation requise)
deploy-production:
if: github.ref == 'refs/heads/main'
needs: build
runs-on:
group: production-runners
labels: [linux, x64]
environment:
name: production
url: https://app.company.com
steps:
- run: kubectl --context production apply -f k8s/

Une architecture de runners se dégrade vite : les machines s'ajoutent au fil des besoins, les labels se multiplient et plus personne ne sait ce que fait un runner donné. Ces quatre habitudes maintiennent le parc compréhensible et, surtout, auditable.

Un label est la seule information dont dispose l'auteur d'un workflow pour choisir sa machine : il doit décrire une capacité ou un périmètre, pas un numéro de série. Un nom vague comme fast finit par désigner la machine la plus lente du parc, sans que personne ose le corriger.

Fenêtre de terminal
# ✅ Labels explicites
--labels linux,x64,docker,production,team-platform
# ❌ Labels vagues
--labels runner1,fast

L'interface GitHub montre les runners et leurs labels, jamais l'intention derrière le découpage ni les règles réseau associées. Sans document de référence, la première question posée lors d'un incident ou d'un audit reste sans réponse. Maintenez un document décrivant :

  • Quels runners existent
  • Leurs labels et capacités
  • Quels repos/équipes peuvent les utiliser
  • Les règles réseau

L'écart se creuse toujours dans le même sens : des labels ajoutés à la main pour débloquer un job, jamais retirés ensuite. Cette requête liste les runners de l'organisation avec leurs labels effectifs, à comparer avec le document de référence.

Fenêtre de terminal
# Lister les runners et leurs labels
gh api /orgs/ORG/actions/runners --jq '.runners[] | {name, labels: [.labels[].name]}'

Un runner configuré à la main dérive : paquets installés au fil des dépannages, label ajouté puis oublié, machine impossible à recréer à l'identique. Décrire le provisionnement en Infrastructure as Code rend le parc reproductible et transforme un changement de labels en modification tracée dans Git. Trois briques se répartissent le travail :

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • L'isolation contrôle quel workflow tourne sur quelle machine, un levier de sécurité et de performance.
  • Les labels routent les jobs ; les groupes de runners restreignent quels dépôts peuvent les utiliser.
  • Séparez au minimum les runners trusted (push, releases) des runners untrusted (PR, forks).
  • Un runner enregistré au niveau dépôt offre l'isolation maximale ; au niveau organisation, il se mutualise via les groupes.
  • Complétez l'isolation logique par une segmentation réseau : chaque environnement dans son sous-réseau.

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