Aperçu
Les runners hébergés par GitHub sont partagés et éphémères par défaut — chaque job obtient une machine virtuelle neuve qui est détruite une fois le job terminé. Les runners self-hosted, en revanche, sont persistants et partagés entre les exécutions de workflows. Cela crée un risque de sécurité important : les secrets, les tokens et les artefacts de build d’un job peuvent fuiter vers le suivant. Un workflow compromis peut empoisonner l’environnement du runner pour tous les jobs futurs.
Actions Runner Controller (ARC) résout ce problème. ARC est un opérateur natif Kubernetes qui vous fournit des runners self-hosted éphémères, auto-scalables et basés sur des conteneurs. Chaque job obtient un pod neuf qui est détruit une fois le job terminé — exactement comme les runners hébergés par GitHub, mais s’exécutant sur votre propre infrastructure avec vos propres outils et politiques réseau.
Dans ce lab pratique, vous allez :
- Déployer ARC sur un cluster Kubernetes local
- Configurer des scale sets de runners éphémères
- Démontrer l’isolation entre jobs (le principal bénéfice de sécurité)
- Construire des images de runner personnalisées
- Implémenter l’isolation par groupe de runners pour la séparation des responsabilités
- Configurer l’autoscaling
- Appliquer des politiques réseau pour restreindre l’accès réseau des runners
Prérequis
Avant de commencer ce lab, assurez-vous de disposer des éléments suivants :
- Cluster Kubernetes — kind, minikube, ou un cluster géré dans le cloud (EKS, GKE, AKS)
- Helm 3 — Installez-le depuis helm.sh
- kubectl — Configuré pour communiquer avec votre cluster
- Compte GitHub — Avec un accès administrateur à un dépôt ou une organisation
- GitHub App ou Personal Access Token (PAT) — Avec les scopes
repoetadmin:org(PAT) ou les permissions GitHub App appropriées - Docker — Pour construire des images de runner personnalisées (Exercice 4)
Configuration de l’environnement
Nous utiliserons kind (Kubernetes in Docker) pour créer un cluster local. Cela garde le lab autonome et facile à nettoyer.
Créer un cluster kind
kind create cluster --name arc-lab
Vérifiez que le cluster fonctionne :
kubectl cluster-info --context kind-arc-lab
Créer un dépôt GitHub de test
Créez un nouveau dépôt (par exemple, arc-lab-test) dans votre compte GitHub. Ajoutez un fichier de workflow simple dans .github/workflows/test.yml :
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Hello from GitHub-hosted runner
run: echo "This runs on a GitHub-hosted runner"
Poussez ceci vers votre dépôt. Nous le modifierons plus tard pour cibler les runners ARC.
Exercice 1 : Installer ARC avec Helm
Actions Runner Controller v2 utilise des charts Helm pour déployer deux composants : un controller qui gère le cycle de vie des pods de runner, et un ou plusieurs runner scale sets qui s’enregistrent auprès de GitHub et acceptent des jobs.
Étape 1 : Ajouter le dépôt Helm
helm repo add actions-runner-controller \
https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
Étape 2 : Configurer l’authentification
ARC doit s’authentifier auprès de l’API GitHub. Vous avez deux options :
Option A : GitHub App (recommandée pour la production)
Créez une GitHub App dans les paramètres de votre organisation ou de votre compte :
- Allez dans Settings → Developer settings → GitHub Apps → New GitHub App
- Définissez les permissions suivantes :
- Repository :
Actions(lecture),Administration(lecture/écriture),Metadata(lecture) - Organization :
Self-hosted runners(lecture/écriture)
- Repository :
- Générez une clé privée et téléchargez-la
- Installez l’App sur votre organisation ou votre dépôt
- Notez l’App ID et l’Installation ID
Option B : Personal Access Token (plus simple pour les labs)
Créez un PAT (classique) avec les scopes repo et admin:org, ou un PAT fine-grained avec les permissions Actions et Administration. Pour ce lab, nous utiliserons un PAT par souci de simplicité.
Étape 3 : Installer le controller ARC
helm install arc \
actions-runner-controller/gha-runner-scale-set-controller \
--namespace arc-systems \
--create-namespace
Vérifiez que le controller fonctionne :
kubectl get pods -n arc-systems
Vous devriez voir une sortie similaire à :
NAME READY STATUS RESTARTS AGE
arc-gha-runner-scale-set-controller-xxx 1/1 Running 0 30s
Étape 4 : Installer un runner scale set
Déployez maintenant un runner scale set qui s’enregistre auprès de votre dépôt GitHub :
helm install arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
--create-namespace \
--set githubConfigUrl="https://github.com/<org>/<repo>" \
--set githubConfigSecret.github_token="<PAT>"
Remplacez <org>/<repo> par le chemin de votre dépôt et <PAT> par votre personal access token.
Vérifiez le runner scale set :
kubectl get pods -n arc-runners
À ce stade, il se peut qu’il n’y ait encore aucun pod de runner — ARC utilise un modèle scale-to-zero. Les pods ne sont créés que lorsque des jobs sont mis en file d’attente.
Étape 5 : Vérifier dans GitHub
Naviguez vers votre dépôt sur GitHub : Settings → Actions → Runners. Vous devriez voir le runner scale set listé avec le nom arc-runner-set. Le statut indique qu’il est prêt à accepter des jobs.
Exercice 2 : Exécuter un workflow sur des runners ARC
Mettez maintenant à jour le workflow de test pour cibler le runner scale set ARC au lieu des runners hébergés par GitHub.
Étape 1 : Mettre à jour le workflow
Modifiez .github/workflows/test.yml pour utiliser le label du runner ARC :
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: arc-runner-set
steps:
- name: Hello from ARC runner
run: |
echo "This runs on an ephemeral ARC runner!"
echo "Hostname: $(hostname)"
echo "Runner OS: $(uname -a)"
- name: Show environment
run: env | sort
Le changement clé est runs-on: arc-runner-set — cela correspond au nom de la release Helm du runner scale set.
Étape 2 : Déclencher le workflow
Poussez le fichier de workflow mis à jour ou utilisez le bouton « Run workflow » (workflow_dispatch) dans l’interface GitHub Actions.
Étape 3 : Observer le pod de runner
Surveillez le namespace arc-runners pendant l’exécution du workflow :
kubectl get pods -n arc-runners -w
Vous verrez un pod créé pour le job :
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 1/1 Running 0 5s
Une fois le job terminé, le pod est arrêté et supprimé :
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 0/1 Completed 0 45s
Exécutez à nouveau kubectl get pods -n arc-runners — le pod a disparu. C’est le modèle éphémère : chaque job obtient un conteneur neuf, et le conteneur est détruit une fois le job terminé. Il n’y a aucune persistance d’état entre les jobs.
Exercice 3 : Démontrer la sécurité éphémère
Cet exercice démontre le principal bénéfice de sécurité des runners éphémères : aucune contamination entre jobs.
Étape 1 : Créer un workflow qui écrit des données sensibles
Créez .github/workflows/ephemeral-test.yml :
name: Ephemeral Security Test
on: workflow_dispatch
jobs:
write-secret:
runs-on: arc-runner-set
steps:
- name: Write sensitive data
run: |
echo "SECRET_API_KEY=sk-prod-abc123xyz" > /tmp/secret-data
echo "DB_PASSWORD=super-secret-password" >> /tmp/secret-data
echo "Written sensitive data to /tmp/secret-data"
cat /tmp/secret-data
read-secret:
runs-on: arc-runner-set
needs: write-secret
steps:
- name: Attempt to read previous job data
run: |
echo "Checking if /tmp/secret-data exists from previous job..."
if [ -f /tmp/secret-data ]; then
echo "SECURITY RISK: Found data from previous job!"
cat /tmp/secret-data
else
echo "SECURE: /tmp/secret-data does not exist."
echo "Each job gets a fresh container — no cross-job contamination."
fi
Étape 2 : Exécuter le workflow
Déclenchez le workflow via workflow_dispatch. Le premier job (write-secret) écrit des données sensibles dans /tmp/secret-data. Le second job (read-secret) s’exécute dans un nouveau pod et tente de lire ce fichier.
Étape 3 : Vérifier les résultats
Dans les logs de GitHub Actions, vous verrez :
- Job write-secret : Écrit le fichier avec succès et affiche son contenu
- Job read-secret : Le fichier n’existe pas — la sortie affiche
SECURE: /tmp/secret-data does not exist.
Chaque job s’est exécuté dans un pod distinct, fraîchement créé. Lorsque le pod write-secret a été détruit, toutes les données — y compris le fichier sensible — ont été détruites avec lui.
Pourquoi c’est important
Sur un runner self-hosted persistant, le fichier /tmp/secret-data serait toujours sur le disque lors de l’exécution du second job. Un workflow malveillant dans une pull request pourrait lire des secrets, des tokens ou des identifiants laissés par des jobs précédents. Avec des runners éphémères, ce vecteur d’attaque est éliminé.
Exercice 4 : Images de runner personnalisées
Les runners ARC utilisent une image de conteneur de base. Pour une utilisation en conditions réelles, vous devez personnaliser cette image afin d’y inclure vos outils de build.
Étape 1 : Créer un Dockerfile personnalisé
Créez un Dockerfile pour votre runner personnalisé :
FROM ghcr.io/actions/actions-runner:latest
USER root
# Install build tools
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
jq \
unzip \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Go
RUN wget -q https://go.dev/dl/go1.22.4.linux-amd64.tar.gz \
&& tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz \
&& rm go1.22.4.linux-amd64.tar.gz
ENV PATH="$PATH:/usr/local/go/bin"
# Install cosign
RUN curl -sSL -o /usr/local/bin/cosign \
https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
&& chmod +x /usr/local/bin/cosign
# Install Docker CLI (for Docker-in-Docker workflows)
RUN curl -fsSL https://get.docker.com | sh
USER runner
Étape 2 : Construire et pousser l’image
# Build the image
docker build -t ghcr.io/<org>/custom-runner:latest .
# Authenticate to GitHub Container Registry
echo "<PAT>" | docker login ghcr.io -u <username> --password-stdin
# Push the image
docker push ghcr.io/<org>/custom-runner:latest
Étape 3 : Configurer ARC pour utiliser l’image personnalisée
Créez un fichier de values custom-runner-values.yaml :
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
Mettez à jour le runner scale set avec l’image personnalisée :
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f custom-runner-values.yaml
Étape 4 : Vérifier les outils personnalisés
Créez un workflow qui utilise les outils personnalisés :
name: Custom Runner Tools Test
on: workflow_dispatch
jobs:
verify-tools:
runs-on: arc-runner-set
steps:
- name: Verify Go
run: go version
- name: Verify cosign
run: cosign version
- name: Verify Docker CLI
run: docker --version
Bénéfice de sécurité : En construisant votre propre image de runner, vous contrôlez exactement quels outils et dépendances sont présents dans l’environnement de build. Il n’y a aucun binaire inattendu, aucun logiciel préinstallé que vous n’avez pas approuvé, et vous pouvez épingler chaque outil à une version spécifique. Vous pouvez également analyser l’image à la recherche de vulnérabilités avant de la déployer.
Exercice 5 : Isolation par groupe de runners
Différents workflows ont différents niveaux de confiance. La validation des pull requests ne devrait pas avoir accès aux secrets de production. Les workflows de déploiement ont besoin de secrets mais ne devraient s’exécuter que depuis la branche main. ARC vous permet d’implémenter cette séparation en créant des runner scale sets distincts avec des labels et des configurations différents.
Étape 1 : Créer un runner scale set de validation des PR
Créez pr-runner-values.yaml :
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "pr-validation"
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
helm install arc-runner-pr \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f pr-runner-values.yaml
Étape 2 : Créer un runner scale set de déploiement
Créez deploy-runner-values.yaml :
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "deployment"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
serviceAccountName: deploy-runner-sa
nodeSelector:
runner-type: deployment
helm install arc-runner-deploy \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f deploy-runner-values.yaml
Étape 3 : Configurer les workflows pour l’isolation
Utilisez des labels de runner différents selon le déclencheur du workflow :
name: CI/CD Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
validate:
if: github.event_name == 'pull_request'
runs-on: arc-runner-pr
steps:
- uses: actions/checkout@v4
- name: Run tests
run: make test
- name: Run linter
run: make lint
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: arc-runner-deploy
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: make deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Cela implémente la séparation des responsabilités au niveau du runner. Les jobs de validation des PR s’exécutent sur des runners qui n’ont aucun accès aux secrets de déploiement ni aux segments réseau privilégiés. Les jobs de déploiement s’exécutent sur un ensemble distinct de runners qui disposent des identifiants et de l’accès réseau nécessaires, mais ne se déclenchent que sur les push vers main.
Exercice 6 : Mise à l’échelle automatique
ARC prend en charge nativement l’autoscaling. Les pods de runner sont créés à la demande et détruits lorsqu’ils sont inactifs. Vous pouvez configurer un nombre minimal et maximal de réplicas pour maîtriser le coût et la réactivité.
Étape 1 : Configurer les paramètres d’autoscaling
Mettez à jour le fichier de values de votre runner scale set pour y inclure les paramètres de mise à l’échelle :
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
minRunners: 0
maxRunners: 10
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f autoscale-values.yaml
Étape 2 : Générer de la charge
Créez un workflow qui déclenche plusieurs jobs en parallèle :
name: Autoscale Test
on: workflow_dispatch
jobs:
parallel-job:
runs-on: arc-runner-set
strategy:
matrix:
id: [1, 2, 3, 4, 5]
steps:
- name: Simulate work
run: |
echo "Job ${{ matrix.id }} running on $(hostname)"
sleep 60
Déclenchez ce workflow et observez la montée en charge des pods :
kubectl get pods -n arc-runners -w
Vous verrez cinq pods créés — un pour chaque job de la matrice :
NAME READY STATUS RESTARTS AGE
arc-runner-set-abcde-runner 1/1 Running 0 5s
arc-runner-set-fghij-runner 1/1 Running 0 5s
arc-runner-set-klmno-runner 1/1 Running 0 5s
arc-runner-set-pqrst-runner 1/1 Running 0 5s
arc-runner-set-uvwxy-runner 1/1 Running 0 5s
Une fois les jobs terminés (60 secondes), tous les pods sont arrêtés. Le namespace revient à zéro pod.
Étape 3 : Configurer le délai de scale-down
Pour optimiser les coûts, vous pouvez souhaiter que les pods restent chauds pendant une courte période après la fin d’un job. Cela évite la latence de démarrage à froid pour les charges de travail en rafale. Le comportement scale-to-zero d’ARC est l’option par défaut et la plus sécurisée. Si vous avez besoin de runners chauds, gardez la fenêtre courte (moins de 5 minutes) et assurez-vous que le mode éphémère reste appliqué.
Exercice 7 : Politiques réseau pour les runners
Les NetworkPolicies de Kubernetes vous permettent de restreindre l’accès réseau des pods de runner. C’est une défense essentielle contre l’exfiltration de données depuis des builds compromis.
Étape 1 : Créer une NetworkPolicy
Appliquez la NetworkPolicy suivante au namespace arc-runners :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress-policy
namespace: arc-runners
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow GitHub API and Actions services
- to:
- ipBlock:
cidr: 140.82.112.0/20
- ipBlock:
cidr: 143.55.64.0/20
- ipBlock:
cidr: 185.199.108.0/22
- ipBlock:
cidr: 4.0.0.0/8
ports:
- protocol: TCP
port: 443
# Allow your container registry (example: ghcr.io)
- to:
- ipBlock:
cidr: 140.82.112.0/20
ports:
- protocol: TCP
port: 443
# Allow your artifact storage (replace with your CIDR)
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - protocol: TCP
# port: 443
kubectl apply -f runner-network-policy.yaml
Note : GitHub publie ses plages d’adresses IP à l’adresse https://api.github.com/meta. Utilisez les plages actions et api. Les CIDR ci-dessus sont des exemples — vérifiez les plages actuelles et mettez-les à jour en conséquence.
Étape 2 : Tester la NetworkPolicy
Créez un workflow qui tente d’atteindre une URL externe :
name: Network Policy Test
on: workflow_dispatch
jobs:
test-network:
runs-on: arc-runner-set
steps:
- name: Test GitHub API (should work)
run: curl -s -o /dev/null -w "%{http_code}" https://api.github.com
- name: Test external URL (should be blocked)
run: |
if curl -s --connect-timeout 5 https://evil-exfiltration-server.example.com; then
echo "FAIL: External access was allowed"
exit 1
else
echo "PASS: External access was blocked by NetworkPolicy"
fi
Lorsque vous exécutez ce workflow :
- La requête vers l’API GitHub réussit (HTTP 200) parce que la NetworkPolicy autorise le trafic vers les plages d’adresses IP de GitHub.
- La requête vers l’URL externe expire et échoue parce qu’elle ne figure pas dans la liste d’egress autorisée.
Cela empêche un build compromis d’exfiltrer du code source, des secrets ou des artefacts de build vers un serveur contrôlé par un attaquant. Même si une dépendance malveillante exécute du code arbitraire pendant le build, elle ne peut pas communiquer avec l’extérieur.
Nettoyage
Supprimez toutes les ressources créées au cours de ce lab :
# Delete Helm releases
helm uninstall arc-runner-set -n arc-runners
helm uninstall arc-runner-pr -n arc-runners
helm uninstall arc-runner-deploy -n arc-runners
helm uninstall arc -n arc-systems
# Delete namespaces
kubectl delete namespace arc-runners
kubectl delete namespace arc-systems
# Delete the kind cluster
kind delete cluster --name arc-lab
Si vous avez créé une GitHub App pour ce lab, vous pouvez la supprimer depuis Settings → Developer settings → GitHub Apps. Révoquez tous les PAT que vous avez créés.
Points clés à retenir
- Les runners éphémères éliminent la contamination entre jobs. Chaque job obtient un conteneur neuf — les secrets, les tokens et les artefacts de build sont détruits une fois le job terminé.
- ARC apporte les bénéfices des runners self-hosted sans les risques de sécurité. Vous bénéficiez d’outils personnalisés, d’un accès à un réseau privé et de la maîtrise des coûts tout en conservant le modèle de sécurité éphémère.
- Les images de runner personnalisées vous donnent un contrôle total sur l’environnement de build. Épinglez les versions des outils, analysez-les à la recherche de vulnérabilités et éliminez le risque lié à la chaîne d’approvisionnement des logiciels préinstallés.
- L’isolation par groupe de runners implémente la séparation des responsabilités. Les workflows de validation de PR et de déploiement s’exécutent sur des ensembles de runners distincts, avec des privilèges et des accès réseau différents.
- Les politiques réseau constituent une couche de défense essentielle. Restreindre l’egress des runners empêche l’exfiltration de données même si une étape de build est compromise.
- L’autoscaling scale-to-zero réduit le coût et la surface d’attaque. Les pods de runner n’existent que pour la durée d’un job — il n’y a aucune infrastructure persistante à maintenir ou à sécuriser.
Prochaines étapes
Continuez à renforcer la posture de sécurité de votre CI/CD avec ces guides connexes :
- Sécuriser les runners GitHub Actions — Plongée approfondie dans les bonnes pratiques de sécurité des runners, la gestion des tokens et la surveillance, pour les runners hébergés par GitHub comme pour les runners self-hosted.
- Séparation des responsabilités et moindre privilège dans les pipelines CI/CD — Guide complet pour appliquer les principes de moindre privilège à l’ensemble de votre pipeline CI/CD, du contrôle de source au déploiement en production.