Installer un runner GitHub Actions sur un VPS
Installez un runner GitHub Actions auto-hébergé sur Ubuntu 24.04 avec utilisateur dédié, checksum, config.sh et service systemd. Voyez le risque des PR de forks.
Ce que fait un runner GitHub Actions auto-hébergé
Un runner GitHub Actions auto-hébergé est un programme que vous installez sur votre propre VPS. Il demande des jobs à GitHub et les exécute sur votre matériel. Vous l’enregistrez auprès d’un dépôt, vous l’installez comme service systemd, puis il redémarre après chaque reboot. GitHub planifie le job. Votre serveur exécute le travail.
La CI (intégration continue) sur un serveur que vous contrôlez présente deux avantages. Les minutes de build ne sont plus décomptées. Un job peut aussi accéder à des ressources disponibles uniquement sur votre machine, comme un cache de build encore chaud ou un réseau privé. Le prix à payer est la sécurité. Le runner exécute ce que le fichier de workflow indique, avec les droits de l’utilisateur que vous lui avez attribué. Un fichier de workflow constitue donc, par conception, une exécution de code à distance. Dans un dépôt privé, ce fonctionnement est acceptable, car seules les personnes de confiance peuvent en ajouter un. Dans un dépôt public, il présente un risque réel. La section consacrée aux pull requests issues de forks explique ce mécanisme.
Tout ce qui suit concerne Ubuntu 24.04 et la version 2.336.0 du runner, la version actuellement publiée en juillet 2026.
Ce dont vous avez besoin avant de commencer
Partez d’un VPS avec un compte d’administration standard et sudo, dans l’état décrit dans les dix premières minutes sur un nouveau VPS. Vous n’avez pas besoin d’ouvrir de port entrant. Le runner ouvre une connexion HTTPS (hypertext transfer protocol secure) sortante vers GitHub et la maintient ouverte en attendant du travail. GitHub ne se connecte donc jamais à votre serveur. Votre pare-feu peut rester fermé au trafic externe et les jobs continuent d’arriver.
Vous devez également disposer des droits d’administration sur le dépôt, car le registration token est affiché dans les paramètres du dépôt.
Créer un utilisateur dédié pour le runner
N’exécutez jamais le runner avec root ou avec votre propre compte d’administration. Chaque job hérite des droits de l’utilisateur du runner. Un workflow qui appelle sudo réussit donc si l’utilisateur du runner peut utiliser sudo. Créez un utilisateur non privilégié qui ne possède rien, à l’exception de son propre répertoire personnel. La section Comptes utilisateur avec le principe du moindre privilège sur un VPS présente le principe général. Voici la configuration adaptée à ce cas.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l verrouille le mot de passe. Personne ne peut donc se connecter en tant que gharunner avec ce mot de passe. Le mode 700 sur le répertoire du runner est important, car le runner y stocke ses identifiants en clair et un checkout peut contenir du code source privé.
Vérifiez ces deux propriétés avant de continuer :
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S affiche une ligne qui commence par gharunner L. L signifie que le mot de passe est verrouillé. sudo -l -U gharunner doit répondre avec is not allowed to run sudo. S’il affiche plutôt une liste de commandes autorisées, le compte appartient à un groupe sudo et l’isolation que vous venez de mettre en place n’existe plus.
Téléchargez le runner et vérifiez l’archive tar
À partir de maintenant, utilisez l’utilisateur runner.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"Exécutez d’abord uname -m si vous n’êtes pas certain de l’architecture. x86_64 utilise le fichier linux-x64 ci-dessus. aarch64 utilise actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Vérifiez maintenant le fichier téléchargé. La valeur SHA256 (secure hash algorithm, 256 bits) ci-dessous correspond à l’archive tar x64 en version 2.336.0. GitHub affiche la valeur de la version actuelle sur la page de la release et dans l’écran New self-hosted runner. Elle change à chaque version. Copiez-la donc depuis ces emplacements lorsque vous installez une autre version.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cUn téléchargement correct affiche une ligne :
actions-runner-linux-x64-2.336.0.tar.gz: OKUn fichier tronqué ou modifié affiche l’échec et un avertissement :
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNe passez pas cette vérification et ne laissez pas tar détecter le problème à votre place. Une archive écrite partiellement échoue avec gzip: stdin: unexpected end of file et tar: Unexpected EOF in archive. Cela indique que le fichier est corrompu, mais pas s’il a été tronqué ou remplacé.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsContenu de l’archive tar et éléments absents
Après l’extraction, le répertoire contient config.sh, run.sh, env.sh, safe_sleep.sh, bin/ et externals/. bin/ contient les binaires du runner et bin/installdependencies.sh. externals/ contient le runtime Node fourni avec le bundle, que les actions JavaScript utilisent.
Il n’y a pas encore de svc.sh. La documentation GitHub le décrit comme le script « créé après l’ajout réussi du runner », car il est généré à partir d’un modèle qui intègre votre dépôt et le nom du runner dans le nom du service. sudo ./svc.sh install avant ./config.sh échoue donc avec sudo: ./svc.sh: command not found. Enregistrez d’abord le runner, puis installez le service.
Installer les dépendances du runner
Le runner est une application .NET. Il a donc besoin de quelques bibliothèques partagées. Conservez le shell de l’utilisateur du runner et installez-les avec sudo, car le script écrit dans la base de données des paquets système.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shSur Ubuntu 24.04, cette commande installe libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 et libicu74. Pour chaque bibliothèque, le script essaie plusieurs noms de version et conserve celui fourni par votre release. C’est pourquoi le même script fonctionne sur les anciennes versions d’Ubuntu et sur Debian.
Si vous ignorez cette étape, ./config.sh s’arrête avant toute autre opération :
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.L’absence de libicu produit le même conseil, avec une première ligne différente : Libicu's dependencies is missing for Dotnet Core 6.0. Les deux messages ont la même origine : config.sh exécute ldd sur les bibliothèques fournies avant de démarrer. Un lien non résolu arrête donc le script au lieu de provoquer un crash difficile à diagnostiquer plus tard.
Enregistrez le runner auprès de votre dépôt
Récupérez un token dans le dépôt. Ouvrez Settings, puis Actions, puis Runners, puis New self-hosted runner. La page affiche un token d’enregistrement qui commence par A. Il expire une heure après sa création. Générez-le donc lorsque vous êtes prêt à le coller.
Effectuez l’enregistrement avec l’utilisateur du runner. config.sh refuse de s’exécuter avec sudo.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceVoici le rôle de ces flags. --name correspond au nom sous lequel le runner apparaît dans le dépôt. Choisissez un nom que vous reconnaîtrez encore dans six mois. --labels ajoute vos propres labels. Le runner possède déjà self-hosted, Linux et X64 sans configuration supplémentaire. --work indique le répertoire dans lequel les checkouts sont placés, à l’intérieur du répertoire du runner. --unattended répond aux invites interactives avec leurs valeurs par défaut. C’est le comportement attendu lorsque la commande se trouve dans un script. --replace reprend l’enregistrement existant portant le même nom au lieu d’échouer. C’est utile lorsque vous reconstruisez le serveur.
Une exécution réussie se termine par les lignes suivantes :
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.L’enregistrement se trouve désormais dans le répertoire du runner, sous la forme de .runner, .credentials et .credentials_rsaparams. Les deux derniers fichiers identifient ce runner auprès de GitHub. Toute personne pouvant les lire peut donc usurper son identité. C’est pourquoi le répertoire est en mode 700 et l’utilisateur ne dispose pas de sudo.
Installer le runner comme service systemd
./run.sh dans un terminal convient pour un seul test, mais le processus s’arrête avec votre session SSH. Installez le service pour que le runner démarre au boot. Services et timers systemd sur un VPS explique les fichiers d’unité eux-mêmes. Ici, svc.sh en crée un pour vous.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh nécessite les droits root, car il écrit une unité dans /etc/systemd/system et l’active. L’argument qui suit install correspond à l’utilisateur sous lequel le service s’exécute. Indiquez explicitement gharunner. Sans argument, le script utilise $SUDO_USER, qui est votre compte d’administration. Chaque job s’exécute alors avec un utilisateur qui peut utiliser sudo.
L’unité porte le nom du dépôt et du runner, sous la forme actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Vous n’avez jamais besoin de le saisir manuellement :
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerUn runner opérationnel journalise √ Connected to GitHub, puis une ligne qui se termine par Listening for Jobs. La page Runners du dépôt l’affiche alors comme Idle. Un runner affiché comme Offline n’est pas démarré ou ne peut pas joindre GitHub sur le port 443.
Envoyer un job au runner
runs-on sélectionne un runner selon son label. Demandez self-hosted ainsi que votre propre label, afin qu’un job ne soit pas exécuté sur un runner qui n’était pas prévu.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aSi le job reste en attente à Waiting for a runner to pick up this job, les labels ne correspondent pas. Chaque label de runs-on doit exister sur le runner. Un seul mot en trop laisse le job en file d’attente, sans erreur visible. Comparez la liste avec les labels affichés à côté du runner dans les paramètres du repository.
Pourquoi les runners auto-hébergés et les dépôts publics sont incompatibles
C’est la partie que beaucoup ignorent. Les recommandations de GitHub sont claires : les runners auto-hébergés « ne devraient presque jamais être utilisés pour des dépôts publics » et GitHub « ne garantit pas leur exécution dans des machines virtuelles éphémères et propres ; du code non fiable exécuté dans un workflow peut les compromettre durablement ».
Le mécanisme est simple. Une pull request provenant d’un fork contient sa propre copie du fichier de workflow. Si votre dépôt public exécute les workflows de pull request sur votre runner, toute personne capable de forker le dépôt peut proposer un workflow qui exécute ses commandes sur votre VPS. Elle n’a besoin d’aucun accès en écriture, car l’élément qu’elle propose est précisément celui qui s’exécute.
Les paramètres d’approbation limitent ce risque sans le supprimer. Par défaut, pour un dépôt public, GitHub demande à un mainteneur d’approuver le workflow d’un fork appartenant à un contributeur qui intervient pour la première fois. Après cette première approbation, les pull requests suivantes de cette personne s’exécutent sans nouvelle demande. Le contrôle repose donc sur une personne qui doit examiner un diff à chaque fois, et il est facile de ne pas repérer une charge malveillante dissimulée trois niveaux plus bas dans un script de build.
Une pull request provenant d’un fork ne reçoit pas vos secrets, et son GITHUB_TOKEN est en lecture seule. Cela limite les dégâts dans GitHub. Cela ne protège pas votre serveur. L’attaquant dispose d’un shell sous l’identité de gharunner. Il peut donc lire tous les fichiers accessibles à cet utilisateur, atteindre tout ce que le VPS peut atteindre sur son réseau privé et laisser un élément persistant dans ~/.bashrc ou dans une unité systemd utilisateur qui s’exécutera lors du prochain job.
L’enregistrement avec --ephemeral fait accepter un seul job au runner, puis le désenregistre. Un job ne peut donc pas lire le workspace du job suivant. Cela n’est utile que si la machine ou le conteneur est recréé pour chaque job, car une backdoor écrite dans le répertoire personnel de l’utilisateur du runner survit à une nouvelle inscription.
Les règles à retenir sont courtes. Utilisez des runners auto-hébergés pour les dépôts privés. Si vous devez en rattacher un à un dépôt public, n’y exécutez pas les pull requests provenant de forks, ne faites rien d’autre tourner sur ce serveur et considérez la machine comme jetable.
Les tâches Docker et le groupe qui équivaut réellement à root
Les tâches exécutées dans des conteneurs, les conteneurs de service et toute étape de workflow qui appelle docker build nécessitent un daemon Docker sur l’hôte du runner. Installez Docker de la manière habituelle, comme l’explique Docker et Docker Compose sur un VPS, puis ajoutez l’utilisateur du runner au groupe docker.
Comprenez le compromis avant de procéder. L’appartenance au groupe docker équivaut à root, car un conteneur peut monter / avec bind mount et s’exécuter en tant que root à l’intérieur. Un workflow qui peut communiquer avec le socket Docker peut donc lire et modifier tous les fichiers du VPS, y compris /etc/shadow. Dans un dépôt privé avec des contributeurs de confiance, ce compromis peut être acceptable. Ailleurs, il annule l’intérêt d’un utilisateur sans privilèges. Docker rootless maintient les builds de conteneurs dans les droits propres à l’utilisateur du runner, au prix d’un storage driver plus lent et de l’absence de conteneurs privilégiés.
Mises à jour et suppression propre du runner
Par défaut, un self-hosted runner se met à jour automatiquement. Il détecte une nouvelle release, remplace ses propres fichiers et redémarre le service. Vous n’avez donc normalement rien à faire. ./config.sh --disableupdate désactive l’auto-mise à jour lorsque vous devez conserver une version fixe. Après cela, vous devez effectuer les mises à jour vous-même : la documentation GitHub précise qu’un runner configuré avec --disableupdate doit être mis à jour manuellement.
Une mise à jour manuelle conserve l’enregistrement, car .runner et .credentials ne se trouvent pas dans l’archive tar. Arrêtez le service, téléchargez la nouvelle archive tar et vérifiez son checksum avec gharunner, extrayez-la dans le même répertoire avec tar xzf, puis redémarrez le service :
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startPour supprimer le runner, désinstallez d’abord le service, puis supprimez l’enregistrement. Le removal token se trouve sur la même page Runners, sous le bouton Remove du runner concerné.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERESupprimer le répertoire sans supprimer l’enregistrement laisse le runner affiché comme Offline dans le repository, car GitHub n’apprend sa suppression que lorsque le runner le signale ou qu’un administrateur supprime l’entrée manuellement.
Modes d’échec et messages affichés
Must not run with sudo. config.sh affiche ce message et se termine lorsqu’il est exécuté en tant que root. Ce contrôle est volontaire, car des fichiers appartenant à root dans _work bloquent tous les jobs suivants exécutés avec le compte de service. Exécutez ./config.sh en tant que gharunner. La variable RUNNER_ALLOW_RUNASROOT permet de contourner ce contrôle, mais son utilisation ne fait que repousser le problème.
sudo: ./svc.sh: command not found. Vous vous trouvez dans le bon répertoire. svc.sh n’existe pas encore, car config.sh n’a pas terminé l’enregistrement. Enregistrez le runner, puis installez le service.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Le token n’est pas un token d’enregistrement valide. Il a peut-être expiré, car sa durée de validité est limitée à une heure, ou bien un personal access token a été collé à la place du token d’enregistrement disponible sur la page Runners. Générez un nouveau token et collez-le à nouveau.
Dependencies is missing for Dotnet Core 6.0. Exécutez sudo ./bin/installdependencies.sh depuis le répertoire du runner en tant que root, puis recommencez l’enregistrement.
Runner hors ligne après un redémarrage. Exécutez systemctl is-enabled 'actions.runner.*'. Si rien n’est affiché, ./svc.sh install n’a jamais été exécuté : le runner n’a donc existé que dans votre session de terminal. Si l’unité est activée et que le runner est toujours hors ligne, consultez journalctl -u 'actions.runner.*' et vérifiez les connexions HTTPS sortantes.
Le disque se remplit. Les checkouts, les caches de build et les images Docker s’accumulent dans _work et dans le répertoire personnel de l’utilisateur du runner. Aucun nettoyage automatique n’est effectué. Surveillez du -sh /home/gharunner/actions-runner/_work et planifiez un nettoyage avant que le disque ne soit plein.
FAQ
Pourquoi sudo ./svc.sh install indique-t-il que la commande est introuvable ?
Parce que svc.sh ne se trouve pas dans l’archive tarball du runner. Il est généré dans le répertoire du runner lorsque ./config.sh termine l’enregistrement, en utilisant le nom de votre dépôt et celui du runner pour construire le nom du service. Exécutez d’abord ./config.sh avec l’utilisateur du runner. Ensuite, sudo ./svc.sh install gharunner trouve le script et écrit une unité nommée actions.runner.OWNER-REPO.RUNNER-NAME.service dans /etc/systemd/system.
Dois-je ouvrir un port du firewall pour un runner auto-hébergé ?
Non. Le runner ouvre une connexion HTTPS sortante vers GitHub et la maintient ouverte en attendant les jobs. GitHub n’initie donc jamais de connexion vers votre VPS. Autorisez les connexions sortantes sur le port 443 et laissez vos règles entrantes fermées. Si le runner affiche Offline alors que son service fonctionne, vérifiez le filtrage sortant et le DNS plutôt que les règles entrantes.
Puis-je utiliser un runner auto-hébergé avec un dépôt public ?
Oui, mais GitHub le déconseille. Une pull request provenant d’un fork contient son propre fichier de workflow. Toute personne pouvant forker votre dépôt peut donc proposer des commandes qui s’exécuteront sur votre machine. La demande d’approbation ne concerne que la première exécution d’un contributeur. Si vous rattachez un runner à un dépôt public, désactivez les workflows des pull requests provenant de forks sur ce runner, ne stockez rien d’autre sur ce serveur et reconstruisez la machine régulièrement.
Pourquoi l’enregistrement échoue-t-il avec Http response code: NotFound ?
L’appel d’enregistrement renvoie NotFound lorsque l’identifiant est incorrect, et pas uniquement lorsque l’URL est incorrecte. Le message est donc trompeur. Les registration tokens expirent une heure après leur affichage, et un personal access token n’est pas accepté pour cet appel. Ouvrez de nouveau Settings, Actions, Runners, New self-hosted runner, copiez le nouveau token et vérifiez que la valeur --url pointe vers un dépôt sur lequel vous disposez des droits d’administration.