Pour exploiter un runner Forgejo en sécurité, on vérifie le binaire 13.2.0
par empreinte et par signature, on l'enregistre hors ligne avec
forgejo-cli actions register, on lui donne deux labels (docker et host),
puis on constate ce que chaque exécuteur isole vraiment. Le runner est le
programme qui exécute les workflows de la forge, l'exécuteur est l'endroit où
un job tourne et le label est le nom que le workflow demande (la première
section les définit). Un job host
tourne sur la machine, un job docker tourne en root dans un conteneur dont
le réseau reste ouvert, et le mode éphémère limite un runner à un seul job.
La leçon s'adresse à l'administrateur dont la forge tourne déjà et qui veut y
brancher de la CI sans ouvrir une porte dérobée.
Vous repartez avec cinq preuves : un binaire vérifié par deux
méthodes, un runner en ligne vu par l'API, un job docker vert dont on
a mesuré les limites, un job host qui lit le jeton du runner, et un
runner éphémère qui disparaît après un job. Les commandes ont été
exécutées avec Forgejo 15.0.10 et le runner 13.2.0, sur AlmaLinux 10 avec
SELinux en mode enforcing, le runner étant sur la même machine virtuelle que
la forge.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Vérifier le binaire du runner par
sha256sumet par signature GPG. - Enregistrer un runner sans les commandes dépréciées, avec un secret passé par l'entrée standard.
- Configurer les labels, la confiance envers la CA de la forge et les volumes autorisés.
- Comparer les exécuteurs
dockerethosten mesurant ce qu'un job peut atteindre. - Repérer les écarts de Forgejo Actions avec GitHub Actions qui font échouer une migration.
- Limiter un runner à un seul job avec le mode éphémère.
Quels sont les prérequis ?
Section intitulée « Quels sont les prérequis ? »Il vous faut la forge des leçons précédentes : Forgejo 15 LTS derrière
Caddy dans /opt/forgejo-lab, le nom forge.lab.test et le compte
fjadmin
(installation), avec les
fichiers Compose de surcharge expliqués dans
Configuration initiale.
La notion de jeton à portée limitée est celle de
Sécuriser l'instance, et
la révocation d'un jeton celle de
Assister les utilisateurs.
Vous devez savoir lire un code HTTP avec curl, écrire un fichier en
root avec sudo, et lire un workflow (un fichier YAML qui décrit les
étapes d'une intégration continue). Le principe qui guide les choix de la
leçon est celui du
moindre privilège. Si vous
connaissez déjà GitHub Actions, vous
reconnaîtrez beaucoup de choses, et c'est un piège : la section sur les écarts
montre où l'analogie casse.
La VM doit d'abord faire confiance à la CA de Caddy (l'autorité de
certification qui a signé le certificat de forge.lab.test), sinon toute
connexion HTTPS échoue. Avant l'ajout, un curl depuis la VM répond
000 avec une erreur de certificat ; après, HTTP 200.
sudo docker cp forgejo-proxy:/data/caddy/pki/authorities/local/root.crt /tmp/forge-lab-ca.crtsudo install -m 0644 /tmp/forge-lab-ca.crt /etc/pki/ca-trust/source/anchors/forge-lab-ca.crtsudo update-ca-trustcurl -fsS -m 10 -o /dev/null -w 'HTTP %{http_code}\n' https://forge.lab.test/api/v1/versionLa dernière commande affiche HTTP 200. Préparez ensuite le shell de la
VM : le nom de la forge, l'adresse de l'hôte (celle
du lab, à remplacer par la vôtre), un jeton d'administrateur obtenu avec le
mot de passe de fjadmin (le .env du lab le conserve sous ADMIN_PASSWORD),
et la fonction api. Le jeton de portée all ne convient qu'à un lab
jetable.
D=forge.lab.testAPI=https://$D/api/v1PW=$(sudo grep '^ADMIN_PASSWORD=' /opt/forgejo-lab/.env | cut -d= -f2-)T=$(curl -fsS -u "fjadmin:$PW" -H 'Content-Type: application/json' -X POST "$API/users/fjadmin/tokens" -d '{"name":"l8tok","scopes":["all"]}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["sha1"])')unset PWapi() { curl -sS -H "Authorization: token $T" -H 'Content-Type: application/json' "$@"; }La variable T contient un secret : ne l'affichez jamais, et révoquez
le jeton en fin de leçon (section « Comment défaire cette configuration ? »).
Qu'est-ce qu'un runner Forgejo, un exécuteur et un label ?
Section intitulée « Qu'est-ce qu'un runner Forgejo, un exécuteur et un label ? »Un runner est un programme, forgejo-runner, qui interroge la forge, récupère
les jobs d'un workflow et les exécute ; l'exécuteur décide où le job tourne, et
le label relie le runs-on d'un workflow à cet exécuteur. Forgejo Actions
est le moteur de CI intégré à la forge. Il ne lance rien lui-même : sans
runner, un workflow reste en attente. Le runner se connecte à la forge, jamais
l'inverse, et il le fait en interrogeant l'API à intervalle régulier
(fetch_interval, 2 secondes par défaut dans la configuration générée).
Deux exécuteurs servent dans la leçon. L'exécuteur docker lance le job
dans un conteneur créé à partir d'une image. L'exécuteur host lance
les commandes directement sur la machine, avec le compte système du runner. Un
troisième, lxc, existe : il n'est pas disponible sur AlmaLinux 10 sans
installation tierce (which lxc lxc-ls incus ne trouve rien), et nous ne
l'avons pas essayé.
Un label est un nom, comme docker, suivi après deux-points de
l'exécuteur et de l'image : docker:docker://<image> ou host:host. Un
workflow écrit runs-on: docker ; la forge confie le job à un runner qui
déclare ce label. Le runner n'est pas un détail de plomberie : c'est le
composant qui exécute du code fourni par les utilisateurs de la forge. Les principes généraux de sécurité des
runners auto-hébergés sont ceux de
Sécuriser les runners self-hosted :
cette leçon les applique au runner Forgejo sans les redire.
Comment installer le runner en vérifiant le binaire ?
Section intitulée « Comment installer le runner en vérifiant le binaire ? »On télécharge le binaire, son fichier .sha256 et sa signature .asc, on
contrôle l'empreinte avec sha256sum -c, puis la signature avec gpg --verify en comparant l'empreinte de la clé primaire à celle de la
documentation. L'empreinte seule prouve que le fichier n'a pas été
corrompu ; la signature prouve qu'il a été publié par les mainteneurs. Les deux
contrôles se complètent. La version 13.2.0 date du 2026-09-18 et la liste des
publications est celle de l'API de code.forgejo.org.
V=13.2.0B=https://code.forgejo.org/forgejo/runner/releases/download/v$VW=$(mktemp -d); cd "$W"for f in forgejo-runner-$V-linux-amd64 forgejo-runner-$V-linux-amd64.sha256 forgejo-runner-$V-linux-amd64.asc; do curl -fsSL -O "$B/$f"; donesha256sum -c forgejo-runner-$V-linux-amd64.sha256La sortie est forgejo-runner-13.2.0-linux-amd64: OK. Passez à la
signature, avec un trousseau jetable pour ne pas mêler la clé à celui de votre
compte.
export GNUPGHOME=$W/g; mkdir -m700 $GNUPGHOMEgpg --keyserver hkps://keys.openpgp.org --recv EB114F5E6C0DC2BCDD183550A4B61A2DC5923710gpg --verify forgejo-runner-$V-linux-amd64.asc forgejo-runner-$V-linux-amd64La sortie contient Good signature from "Forgejo <contact@forgejo.org>",
puis l'empreinte de la clé primaire,
Primary key fingerprint: EB11 4F5E 6C0D C2BC DD18 3550 A4B6 1A2D C592 3710.
La signature est faite par une sous-clé (3BF4 E813 … 6B67 50) de
cette clé primaire. Le message WARNING: This key is not certified with a trusted signature est normal : il dit que votre trousseau ne connaît pas la
clé par une chaîne de confiance. Ce qui vous protège est la comparaison de
l'empreinte de la clé primaire avec celle de la documentation du runner.
Le piège que nous avons rencontré : l'adresse code.forgejo.org/forgejo.gpg
ne contenait aucune clé (This user hasn't uploaded any GPG keys), et
gpg --verify répondait Can't check signature: No public key. Allez chercher
la clé primaire par son empreinte sur keys.openpgp.org. Installez ensuite le
binaire, créez le compte système et ajoutez-le au groupe docker.
sudo install -m 0755 -o root -g root forgejo-runner-$V-linux-amd64 /usr/local/bin/forgejo-runnerforgejo-runner -vid runner 2>/dev/null || sudo useradd --create-home runnersudo usermod -aG docker runnerid runnercd /; rm -rf "$W"; unset GNUPGHOMELes sorties sont forgejo-runner version v13.2.0 et groups=…,994(docker)
pour runner (le numéro du groupe varie d'une machine à l'autre). Retenez
cette dernière ligne : appartenir au groupe docker revient à pouvoir
contrôler le démon Docker de la machine, ce que la section sur l'exécuteur
host met en évidence.
Comment enregistrer le runner sans commande dépréciée ?
Section intitulée « Comment enregistrer le runner sans commande dépréciée ? »Dans la version 13.2, forgejo-runner register et create-runner-file sont
dépréciés (l'aide l'indique) ; la voie jouée ici est l'enregistrement hors
ligne : l'instance génère un secret, le déclare avec forgejo forgejo-cli actions register, et le runner s'en sert comme jeton. Ce secret est le
jeton d'enregistrement du runner : une chaîne hexadécimale de 40
caractères qui prouve à la forge que le runner est celui qu'on a déclaré. Il se
passe par l'entrée standard avec --secret-stdin, pas en argument, car un
argument est visible dans la liste des processus.
Le schéma suit l'enregistrement de haut en bas, entre quatre acteurs.
L'administrateur fait générer le secret par l'instance, puis le déclare
avec ses labels ; l'instance répond par l'UUID du runner (son identifiant
unique, dérivé du secret). Ces valeurs vont dans config.yml, que le démon
(le processus forgejo-runner daemon, qui tourne en continu) lit au démarrage
pour se déclarer, puis il attend un job dont le runs-on correspond à
l'un de ses labels.
Avant de jouer ces étapes, deux préparations. La CA de Caddy est déjà dans
le magasin de confiance de la VM (voir la section des prérequis). Il faut
maintenant une copie dédiée dans /etc/forgejo-runner/ca.crt, destinée à
être montée dans les conteneurs de job : on ne monte pas un fichier du
magasin de confiance de l'hôte.
sudo install -d -m 0755 /etc/forgejo-runnersudo install -m 0644 /tmp/forge-lab-ca.crt /etc/forgejo-runner/ca.crtPour l'image du job, relevez ensuite le digest (l'empreinte du
contenu) de l'image Node du miroir data.forgejo.org, afin de l'épingler
dans le label. Le label prend la forme canonique image:tag@sha256:… : le
tag dit ce que l'on croit utiliser, le digest dit ce que l'on utilise vraiment.
IMG=data.forgejo.org/oci/node:22-bookwormsudo docker pull "$IMG"sudo docker image inspect "$IMG" --format '{{index .RepoDigests 0}}'NODE=$IMG@$(sudo docker image inspect "$IMG" --format '{{index .RepoDigests 0}}' | cut -d@ -f2)echo "$NODE"La variable IMG porte le tag une seule fois. La commande inspect affiche
data.forgejo.org/oci/node@sha256:…. La ligne suivante garde le seul
digest de cette sortie (cut -d@ -f2) et le recolle derrière le tag dans
NODE, que la dernière commande affiche :
data.forgejo.org/oci/node:22-bookworm@sha256:0e5f…a2a8 dans notre rejeu.
Votre digest peut différer si l'image a été republiée. Un digest ne reçoit
aucun correctif : relevez-le de nouveau à chaque rafraîchissement de l'image.
Il reste à générer le secret, à le garder dans un fichier lisible du seul
compte runner, et à déclarer le runner à l'instance.
SECRET=$(sudo docker exec -u git forgejo forgejo forgejo-cli actions generate-secret | tr -d '\r\n')printf "%s" "$SECRET" | sudo tee /home/runner/.secret >/dev/null; sudo chown runner:runner /home/runner/.secret; sudo chmod 600 /home/runner/.secretecho "secret: ${#SECRET} caracteres (masque)"UUID=$(printf '%s' "$SECRET" | sudo docker exec -i -u git forgejo forgejo forgejo-cli actions register --secret-stdin --name lab-runner --labels docker,host)unset SECRETLa première ligne affichée est secret: 40 caracteres (masque). La
commande register répond par l'UUID du runner, rangé ici dans UUID.
Comme il dérive du secret, ne le publiez pas. La commande est
idempotente (la rejouer avec le même secret met à jour le même runner), et
sans option --scope le runner est global à l'instance ; --scope propriétaire/dépôt le limiterait à un propriétaire ou à un dépôt, ce que nous
n'avons pas essayé. Le --name lab-runner n'a pas été retenu : l'API
affichera runner, le nom venant du démon.
Comment configurer le runner et le lancer en service ?
Section intitulée « Comment configurer le runner et le lancer en service ? »Le fichier config.yml porte une connexion nommée avec l'URL de la forge,
l'UUID, le secret et les labels ; l'unité systemd lance forgejo-runner daemon -c /home/runner/config.yml sous le compte runner. Il n'y a plus de
fichier .runner en mode démon : la section server.connections le remplace.
Le fichier contient un secret, donc il appartient à runner en mode 600.
TOKEN=$(sudo cat /home/runner/.secret)sudo tee /home/runner/config.yml >/dev/null <<EOFlog: level: inforunner: capacity: 1 envs: NODE_EXTRA_CA_CERTS: /etc/ssl/forge-lab-ca.crt GIT_SSL_CAINFO: /etc/ssl/forge-lab-ca.crtcontainer: docker_host: "-" options: "--add-host=forge.lab.test:10.10.110.11 -v /etc/forgejo-runner/ca.crt:/etc/ssl/forge-lab-ca.crt:ro,z" valid_volumes: - /etc/forgejo-runner/ca.crthost: workdir_parent: /home/runner/.cache/actserver: connections: forge: url: https://forge.lab.test/ uuid: $UUID token: $TOKEN labels: - docker:docker://$NODE - host:hostEOFsudo chown runner:runner /home/runner/config.yml; sudo chmod 600 /home/runner/config.ymlLe heredoc de ce bloc n'est pas quoté (<<EOF) : le shell y injecte $UUID,
$TOKEN et $NODE. capacity: 1 limite le runner à un job à la fois, et
host.workdir_parent fixe le dossier parent des dossiers de travail des jobs host. Pour le
reste, chaque bloc a une raison d'être. runner.envs pose deux variables
dans les jobs, pour que Node et Git fassent confiance à la CA montée.
docker_host: "-" est la valeur par défaut : elle signifie qu'aucun
socket Docker n'est donné aux jobs (le socket /var/run/docker.sock est le
point d'entrée du démon Docker, qui donne le contrôle de la machine à qui
l'atteint). container.options passe des options au conteneur de job :
--add-host fait pointer forge.lab.test vers l'adresse de la VM (dans un
conteneur, 127.0.0.1 désigne le conteneur lui-même, alors que la VM le
résout vers son propre bouclage), et -v …:ro,z monte la CA en lecture seule
avec une étiquette SELinux adaptée.
Le piège est valid_volumes. Sans cette liste (elle est vide par défaut),
le montage de la CA est refusé, et le journal du job contient [/etc/forgejo-runner/ca.crt] is not a valid volume, will be ignored. Au premier essai, nous n'avions pas la
liste : le montage était ignoré et le job a échoué. Déclarez donc chaque
chemin monté dans valid_volumes. Créez l'unité systemd et démarrez.
sudo tee /etc/systemd/system/forgejo-runner.service >/dev/null <<'EOF'[Unit]Description=Forgejo RunnerDocumentation=https://forgejo.org/docs/latest/admin/actions/After=docker.service network-online.targetWants=network-online.target
[Service]ExecStart=/usr/local/bin/forgejo-runner daemon -c /home/runner/config.ymlExecReload=/bin/kill -s HUP $MAINPIDUser=runnerWorkingDirectory=/home/runnerRestart=on-failureTimeoutSec=0RestartSec=10
[Install]WantedBy=multi-user.targetEOFsudo systemctl daemon-reloadsudo systemctl enable --now forgejo-runner.servicesleep 8systemctl is-active forgejo-runnersudo journalctl -u forgejo-runner --no-pager -n 15 | sed "s/$TOKEN/***/g"sudo ausearch -m avc -ts recentLa commande is-active répond active. Le journal contient Starting runner daemon, puis runner: runner, with version: v13.2.0, with labels: [docker host], ephemeral: false, declared successfully et [poller] launched. Le filtre sed masque le secret au cas où il apparaîtrait. La
recherche ausearch -m avc -ts recent affiche <no matches> : SELinux n'a
rien refusé. La preuve qui compte est celle de la forge, pas celle du service.
api "$API/admin/actions/runners" | python3 -c 'import sys,jsond=json.load(sys.stdin)r=d["runners"] if isinstance(d,dict) else dfor x in r: print({k:x.get(k) for k in ("id","name","status","ephemeral","labels","version")})'L'API répond {'id': 4, 'name': 'runner', 'status': 'idle', 'ephemeral': False, 'labels': ['docker', 'host'], 'version': 'v13.2.0'}. L'id dépend
des runners déjà enregistrés sur l'instance (4 dans notre rejeu, 1 sur une
instance neuve). Le statut idle signifie « en ligne, sans job ». C'est la
preuve que le runner est enregistré. Pour changer la configuration, éditez
le fichier puis sudo systemctl restart forgejo-runner : l'unité prévoit aussi
un ExecReload, mais nous n'avons rejoué que le redémarrage.
Comment exécuter un premier job dans un conteneur ?
Section intitulée « Comment exécuter un premier job dans un conteneur ? »On crée un dépôt, on y pousse un workflow dans .forgejo/workflows/ avec
runs-on: docker, et le journal de la forge montre l'image épinglée, le
checkout et Job succeeded. Créez d'abord le dépôt de test et un secret, que
la section sur les secrets exploite. Cherchez aussi le SHA du commit de
l'action actions/checkout : les actions se résolvent sur data.forgejo.org,
et l'épinglage par SHA vaut ici comme ailleurs (voir
Épingler les actions par SHA).
api -X POST "$API/user/repos" -d '{"name":"l8test","auto_init":true,"default_branch":"main"}' -o /dev/null -w 'create repo HTTP %{http_code}\n'api -X PUT "$API/repos/fjadmin/l8test/actions/secrets/MYSECRET" -d '{"data":"topsecret-value-123"}' -o /dev/null -w 'secret HTTP %{http_code}\n'curl -fsS "https://data.forgejo.org/api/v1/repos/actions/checkout/tags?limit=50" | python3 -c 'import sys,jsonfor t in json.load(sys.stdin): if t["name"] in ("v4","v5","v6"): print(t["name"], t["commit"]["sha"])'Les deux créations répondent HTTP 201 (la valeur du secret du lab est
factice : le vôtre ne doit jamais figurer dans une commande). La liste des
étiquettes donne notamment v6 d23441a48e516b6c34aea4fa41551a30e30af803.
Créez le dossier /tmp/wf (mkdir -p /tmp/wf) et écrivez-y le workflow
ci-dessous ; la fonction put, définie après lui, le poussera par l'API.
name: docker-jobon: [push]jobs: build: runs-on: docker steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - run: | echo "--- os"; head -2 /etc/os-release; id echo "--- contexte"; echo "server_url=${{ github.server_url }} actor=${{ github.actor }} runner_os=${{ runner.os }}" echo "--- noms de variables"; env | cut -d= -f1 | grep -E '^(GITHUB_|FORGEJO_|GITEA_|ACTIONS_|CI$)' | sort | tr '\n' ' '; echo echo "--- socket docker dans le job ?"; ls -l /var/run/docker.sock 2>&1 || true echo "--- reseau" curl -sS -m 8 -o /dev/null -w "forge.lab.test:%{http_code}\n" https://forge.lab.test/api/v1/version || true curl -sS -m 8 -o /dev/null -w "hote 10.10.110.11 https:%{http_code}\n" -k https://10.10.110.11/api/v1/version || true timeout 5 bash -c 'echo > /dev/tcp/10.10.110.11/22' && echo "tcp 10.10.110.11:22 OUVERT" || echo "tcp 10.10.110.11:22 ferme" echo "--- secret"; echo "valeur=${{ secrets.MYSECRET }}" echo "--- fin des mesures"Le workflow sert de sonde : l'étape checkout prouve que le runner joint
la forge et l'action, et les echo mesurent ce que le job voit. Enregistrez-le
sous /tmp/wf/docker.yml, puis poussez-le et attendez une minute.
put() { b=$(base64 -w0 "$2"); api -X POST "$API/repos/fjadmin/l8test/contents/$1" -d "{\"content\":\"$b\",\"message\":\"add $1\"}" -o /dev/null -w "push $1 HTTP %{http_code}\n"; }put .forgejo/workflows/docker.yml /tmp/wf/docker.ymlsleep 60api "$API/repos/fjadmin/l8test/actions/tasks" | python3 -c 'import sys,jsonfor t in json.load(sys.stdin).get("workflow_runs",[]): print(t["id"], t["name"], t["status"])'La poussée répond HTTP 201, et la liste affiche 8 build success dans
notre rejeu (le numéro dépend des exécutions déjà faites sur l'instance). Un statut
failure se diagnostique dans le journal du job, décrit juste après.
Où lire le journal d'un job ?
Section intitulée « Où lire le journal d'un job ? »Le journal d'un job est stocké côté instance en .log.zst, un fichier
compressé en zstd, dans /data/gitea/actions_log/<propriétaire>/<dépôt>/.
L'interface web l'affiche aussi, mais le fichier sert quand on veut chercher
dans le texte ou conserver une preuve. La VM AlmaLinux 10 du lab n'avait pas
zstd (son historique dnf en montre l'installation) : la première commande
l'installe, et ne fait rien s'il est déjà présent. Les suivantes copient le
fichier hors du conteneur forgejo, la variable F recevant le journal le plus
récent du dépôt.
sudo dnf -y -q install zstdsudo docker exec forgejo find /data/gitea/actions_log -type f -name '*.zst' | sortF=$(sudo docker exec forgejo find /data/gitea/actions_log/fjadmin/l8test -type f -name '*.zst' | sort | tail -1)sudo docker cp forgejo:$F /tmp/l2.zstzstd -dc /tmp/l2.zst | sed 's/^[0-9T:.Z+-]* //' | grep -v -E '^hint:|^\[command\]|##\[' | cut -c1-210La commande find liste les chemins du type …/l8test/08/8.log.zst, où 8
est le numéro de la tâche et 08 ce numéro en hexadécimal sur deux chiffres
(0a/10.log.zst pour la tâche 10). Le sed retire l'horodatage de chaque
ligne et le grep écarte le bruit du checkout. Voici les lignes utiles de
la tâche réussie ; l'extrait omet les lignes de variables et de curl,
commentées dans les sections qui suivent.
🚀 Start image=data.forgejo.org/oci/node:22-bookworm@sha256:0e5f906573693feaa1e21057ebdcfdb5bd5021f050b2dc7c9deceb629c7da2a8⭐ Run Main actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803--- osPRETTY_NAME="Debian GNU/Linux 12 (bookworm)"uid=0(root) gid=0(root) groups=0(root)--- contexteserver_url=https://forge.lab.test actor=fjadmin runner_os=Linux--- socket docker dans le job ?ls: cannot access '/var/run/docker.sock': No such file or directorytcp 10.10.110.11:22 OUVERT--- secretvaleur=***🏁 Job succeededChaque ligne répond à une question : l'image est bien celle du digest
épinglé, l'action a été prise sur son SHA, le job tourne dans un Debian 12
(le système de l'image, non celui de la VM), et il y est root. Les
sections suivantes exploitent ces mesures.
Pourquoi refuser l'exécuteur host hors d'un lab ?
Section intitulée « Pourquoi refuser l'exécuteur host hors d'un lab ? »Un job host tourne sur la VM avec le compte runner : il écrit hors de son
répertoire de travail, lit le config.yml du runner, donc son jeton, et liste
les conteneurs de la forge parce que runner est dans le groupe docker.
Un utilisateur qui peut pousser un workflow dans un dépôt servi par ce runner
contrôle donc la forge. La documentation du runner (pages d'installation et de
configuration) le dit pour cet exécuteur : « There is no isolation at all and a
single job can permanently destroy the host ». La page de sécurité ajoute que le
jeton du runner est alors accessible au job. Nous avons mesuré cet accès avec un
workflow sur le label host.
name: host-jobon: [push]jobs: h: runs-on: host steps: - run: | whoami; hostname; head -1 /etc/almalinux-release; pwd echo "ecrit-par-le-job" > /tmp/host-proof.txt && echo "ecrit /tmp/host-proof.txt" test -r /home/runner/config.yml && echo "config.yml du runner LISIBLE ($(grep -c token /home/runner/config.yml) ligne token)" docker ps --format '{{.Names}}' 2>&1 | head -3Enregistrez ce fichier sous /tmp/wf/host.yml, poussez-le avec put .forgejo/workflows/host.yml /tmp/wf/host.yml, lisez le journal comme
ci-dessus (le tail -1 désigne le journal le plus récent, donc celui du job
host), puis contrôlez sur la VM avec ls -l /tmp/host-proof.txt. La
sortie du job, telle que nous l'avons obtenue :
runnerforgejo-alma.lab.labAlmaLinux release 10.2 (Lavender Lion)/home/runner/.cache/act/687a17811eb1ee6c/hostexecutorecrit /tmp/host-proof.txtconfig.yml du runner LISIBLE (1 ligne token)forgejoforgejo-proxyforgejo-db🏁 Job succeededLisez-la de haut en bas : le job est runner sur la VM elle-même
(AlmaLinux, pas un conteneur), le fichier /tmp/host-proof.txt écrit par le
job existe ensuite sur l'hôte (ls -l le montre, propriétaire runner), le
config.yml est lisible et contient la ligne du secret, et docker ps
nomme les trois conteneurs de la forge. Un attaquant qui lit ce secret peut se
faire passer pour le runner ; avec l'accès à docker, il peut agir sur la base
de données de la forge.
La règle est simple : pas d'exécuteur host hors d'un lab jetable. Si
vous devez le garder, le compte qui l'exécute ne doit pas être dans le groupe
docker et ne doit pas pouvoir lire de secret, ce que nous n'avons pas essayé
de configurer. Le host reste utile pour apprendre exactement ce que l'on ne
veut pas en production.
Que cloisonne vraiment l'exécuteur docker ?
Section intitulée « Que cloisonne vraiment l'exécuteur docker ? »L'exécuteur docker sépare le système de fichiers du job de celui de l'hôte
et ne donne aucun socket Docker, mais le job est root dans son conteneur et
son réseau n'est pas cloisonné : il joint l'IP privée de la VM, port 22
compris. C'est un progrès net sur host, pas une isolation complète. Trois
mesures, toutes dans le journal du premier job (celui du label docker), le
démontrent.
D'abord, l'identité : uid=0(root) dans le conteneur. C'est un root
confiné par le moteur de conteneurs, pas celui de la VM, mais toute faille
d'évasion le rapprocherait de l'hôte. Ensuite, le socket : ls -l /var/run/docker.sock répond No such file or directory, parce que
docker_host: "-" ne monte rien. Donner le socket à un job (une valeur
d'URL dans docker_host) reviendrait à lui donner le contrôle du démon Docker,
le même risque que l'exécuteur host. La documentation renvoie à une page
dédiée à l'accès Docker, que nous n'avons pas rejouée.
Enfin le réseau : la ligne tcp 10.10.110.11:22 OUVERT montre qu'un job
atteint un port de la VM. L'isolation du réseau d'un job n'est donc pas
automatique ; elle se construit par un filtrage de la sortie (le principe
d'egress filtré décrit dans
Hardening de l'environnement de build).
Nous n'avons pas posé de règle de pare-feu pour la leçon.
Un dernier détail, utile pour le diagnostic. Dans le job, curl https://forge.lab.test échoue (curl: (60) SSL certificate problem, affichage
forge.lab.test:000) alors que le checkout par git réussit : curl
ignore GIT_SSL_CAINFO, la variable que Git lit. Un step qui appelle la
forge avec curl demande donc sa propre configuration de confiance, que nous
n'avons pas essayée. L'appel https://10.10.110.11 en -k échoue de son côté
sur tlsv1 alert internal error : le serveur a répondu, c'est donc une erreur
de négociation TLS et non un blocage réseau.
Les secrets de dépôt sont-ils masqués dans les journaux ?
Section intitulée « Les secrets de dépôt sont-ils masqués dans les journaux ? »Un secret de dépôt s'affiche *** dans le journal quand le job écrit sa
valeur exacte, mais cette protection se limite au masquage de la valeur : une
valeur transformée n'a pas été testée. Le workflow du premier job
écrit echo "valeur=${{ secrets.MYSECRET }}" ; le journal contient
valeur=***. Sur le même principe, le journal du checkout masque l'en-tête
d'authentification qu'il configure (AUTHORIZATION: basic ***).
Conséquence pratique : le masquage n'est pas un coffre-fort. Un secret passé
dans une commande reste lisible par le job lui-même, donc par quiconque peut
modifier le workflow. Écrire le secret transformé (encodé, coupé en morceaux)
dans un journal est un contournement classique que nous n'avons pas rejoué ;
ne comptez pas sur *** pour protéger un secret contre un auteur de workflow
malveillant. La parade est de ne confier à un job que le secret dont il a
besoin, et de ne pas exécuter de workflow venu d'un dépôt qu'on ne maîtrise pas.
En quoi Forgejo Actions diffère-t-il de GitHub Actions ?
Section intitulée « En quoi Forgejo Actions diffère-t-il de GitHub Actions ? »Forgejo Actions est conçu pour être familier à qui connaît GitHub Actions, mais il n'est pas conçu pour être compatible : quatre écarts ont été prouvés dans notre essai. Ne portez donc pas un workflow en le copiant, et ne présumez pas qu'une action du Marketplace se comporte pareil. Le tableau sert de liste de contrôle à une migration.
| Sujet | GitHub Actions | Forgejo Actions (essai) |
|---|---|---|
| Dossier des workflows | .github/workflows/ | .forgejo/workflows/ ; .github/workflows/ est ignoré quand .forgejo/workflows/ existe |
Résolution de uses: | Marketplace de GitHub | data.forgejo.org par défaut (réglage DEFAULT_ACTIONS_URL de l'instance) |
runs-on: ubuntu-latest | runner fourni par GitHub | attente sans fin si aucun runner ne porte ce label |
| Variables d'environnement | GITHUB_* | GITHUB_* et FORGEJO_*, contexte github.* émulé |
Reprenons les écarts un à un. Pour le dossier, nous avons poussé un
.github/workflows/gh.yml dans un dépôt qui avait déjà .forgejo/workflows/ :
la poussée a relancé les workflows .forgejo déjà présents (build et h), et
aucune exécution du fichier .github n'est apparue. Le cas d'un dépôt
qui n'a que .github/workflows/ n'a pas été rejoué : ne dites pas qu'il
fonctionne, essayez-le.
Pour uses:, la ligne du journal est git clone 'https://data.forgejo.org/actions/checkout'. Une action de GitHub n'est pas
trouvée par défaut ; Forgejo peut aussi appeler une action par son adresse
complète, que nous n'avons pas essayée. Pour runs-on, nous avons poussé
un workflow ubuntu-latest : aucun runner ne porte ce label, et le job n'est
jamais exécuté. Aucun message d'erreur n'apparaît, la tâche ne figure
même pas dans la liste de l'API. C'est le piège qu'on rencontre en migrant :
ajoutez le label voulu au runner ou changez le runs-on. Pour les
variables, le journal liste FORGEJO_ACTION, FORGEJO_ACTIONS et
FORGEJO_ACTIONS_RUNNER_VERSION, en plus des ACTIONS_* et de CI.
L'épinglage ne change pas : une action s'épingle sur le SHA du commit, comme dans le workflow du premier job.
Comment limiter un runner à un seul job avec le mode éphémère ?
Section intitulée « Comment limiter un runner à un seul job avec le mode éphémère ? »Un runner éphémère se déclare avec --ephemeral, se lance avec
forgejo-runner one-job et non daemon, prend un seul job, puis la forge le
supprime : un job compromis ne trouve pas de runner réutilisable. La page
de sécurité de Forgejo le recommande aux administrateurs qui créent des runners
à la demande. Le principe est celui des
runners éphémères de GitHub
Actions, avec des commandes qui lui sont propres.
L'enregistrement ajoute --ephemeral. Le runner permanent est arrêté pendant
l'essai, pour qu'il ne prenne pas le job à la place de l'éphémère. Le fichier
config-eph.yml est une copie de config.yml sans la section server :
l'identité du runner (UUID, adresse de la forge, label) passe par les options
de one-job, et le secret y est lu dans un fichier (--token-url file:…) plutôt
que passé en argument. L'option -w (--wait) fait attendre one-job jusqu'à ce qu'une tâche lui soit attribuée.
SECRET=$(sudo docker exec -u git forgejo forgejo forgejo-cli actions generate-secret | tr -d '\r\n')printf '%s' "$SECRET" | sudo -u runner tee /home/runner/eph.tok >/dev/null; sudo chmod 600 /home/runner/eph.tokUUID=$(printf '%s' "$SECRET" | sudo docker exec -i -u git forgejo forgejo forgejo-cli actions register --secret-stdin --name eph --ephemeral --labels docker)unset SECRETapi "$API/admin/actions/runners" | python3 -c 'import sys,jsond=json.load(sys.stdin); r=d["runners"] if isinstance(d,dict) else dfor x in r: print(" ", {k:x.get(k) for k in ("id","name","status","ephemeral")})'sudo systemctl stop forgejo-runnersudo sed '/^server:/,$d' /home/runner/config.yml | sudo -u runner tee /home/runner/config-eph.yml >/dev/nullL'API liste maintenant deux runners : le permanent (idle, ephemeral: False) et un second offline, ephemeral: True, enregistré mais pas
encore connecté. Créez un dépôt avec deux workflows docker, pour avoir deux
jobs en attente.
api -X POST "$API/user/repos" -d '{"name":"l8eph","auto_init":true,"default_branch":"main"}' -o /dev/null -w 'create repo HTTP %{http_code}\n'for i in 1 2; do printf 'name: eph%s\non: [push]\njobs:\n j:\n runs-on: docker\n steps:\n - run: echo job-ephemere-ok-%s\n' $i $i > /tmp/e.yml api -X POST "$API/repos/fjadmin/l8eph/contents/.forgejo/workflows/e$i.yml" -d "{\"content\":\"$(base64 -w0 /tmp/e.yml)\",\"message\":\"e$i\"}" -o /dev/null -w "push e$i HTTP %{http_code}\n"donesudo -u runner bash -c "cd /home/runner && timeout 120 /usr/local/bin/forgejo-runner one-job -c /home/runner/config-eph.yml --url https://forge.lab.test/ --uuid $UUID --token-url file:/home/runner/eph.tok --label 'docker:docker://$NODE' -w"La sortie contient, dans cet ordre : ephemeral: true, declared successfully, single task poller successfully fetched one task, puis
single task poller is shutting down, et le code de retour est 0. Une
seule tâche a été prise (la tâche 15 dans notre rejeu). L'API ne liste plus que le runner
permanent : l'éphémère a disparu. Relancez le runner permanent.
sudo systemctl start forgejo-runner; sleep 35api "$API/repos/fjadmin/l8eph/actions/tasks" | python3 -c 'import sys,jsonfor t in json.load(sys.stdin).get("workflow_runs",[]): print(" ", t["id"], t["name"], t["status"])'La liste affiche les tâches 16 j success et 15 j success (les numéros
dépendent de l'instance) : la plus récente est
celle que le runner permanent a prise après son redémarrage. Chaque job a donc
bien eu son exécution, et le runner éphémère n'en a pas pris deux.
Deux pièges à connaître. Premier piège : one-job lancé depuis un autre
répertoire que /home/runner s'arrête sur Error: invalid configuration: failed to load registration file: open .runner: permission denied (nous le
lancions d'abord depuis un dossier que runner ne peut pas lire) ; lancez la
commande après un cd /home/runner, comme ci-dessus. Second piège : un runner
éphémère qui ne reçoit jamais de job reste enregistré. Le nôtre s'était
attardé dans l'API (offline, ephemeral: True) après un premier essai
manqué, et il se supprime à la main. Ne lancez la commande que si la
liste montre un tel runner, et remplacez <id> par son identifiant : un
numéro copié d'ici pourrait désigner votre runner permanent.
api -X DELETE "$API/admin/actions/runners/<id>" -o /dev/null -w 'suppression runner HTTP %{http_code}\n'La réponse est HTTP 204 (dans notre essai, le runner oublié portait
l'identifiant 2). Ce que la leçon ne
montre pas : créer un runner éphémère à la demande, pour chaque job, par un
orchestrateur. Le mode éphémère est la brique ; l'automatisation qui lance un
one-job par tâche n'a pas été rejouée.
Que faire quand le runner ne se comporte pas comme prévu ?
Section intitulée « Que faire quand le runner ne se comporte pas comme prévu ? »Les échecs de cette leçon se lisent dans le journal du job, dans le journal systemd du runner ou dans la liste des runners de l'API. Le tableau relie chaque symptôme à sa cause.
| Symptôme | Cause | Correction |
|---|---|---|
[…/ca.crt] is not a valid volume, will be ignored dans le journal du job | le chemin monté n'est pas dans container.valid_volumes | déclarer le chemin, redémarrer le runner |
curl: (60) SSL certificate problem dans un job | curl ne lit pas GIT_SSL_CAINFO | donner à curl sa propre CA ; git passe déjà |
curl sur forge.lab.test répond 000 depuis la VM, avant l'enregistrement | la CA de Caddy n'est pas dans le magasin de confiance | update-ca-trust après avoir copié la CA |
Can't check signature: No public key | la clé n'a pas été importée (l'adresse forgejo.gpg ne contenait aucune clé) | importer l'empreinte EB114F5E… depuis keys.openpgp.org |
Un workflow runs-on: ubuntu-latest ne démarre jamais, sans message | aucun runner ne porte ce label | ajouter le label au runner ou changer runs-on |
.github/workflows/… n'est pas exécuté | .forgejo/workflows/ existe et prend le pas | tout mettre dans .forgejo/workflows/ |
open .runner: permission denied au lancement de one-job | commande lancée hors de /home/runner | cd /home/runner avant la commande |
Runner éphémère offline qui reste dans la liste | il n'a jamais reçu de job | DELETE /api/v1/admin/actions/runners/<id> |
zstd introuvable en lisant un journal | l'outil n'est pas installé sur la VM | sudo dnf -y -q install zstd |
Le diagnostic commence toujours par la même question : le runner est-il en
ligne ? Si GET /api/v1/admin/actions/runners ne le montre pas idle, regardez
journalctl -u forgejo-runner. S'il est idle et que le job ne démarre pas,
comparez le runs-on du workflow aux labels de la liste.
Comment défaire cette configuration ?
Section intitulée « Comment défaire cette configuration ? »On supprime les dépôts de test, on arrête et retire l'unité, on retire le
runner de l'API, on révoque le jeton, puis on retire le compte, le binaire et la
CA : ce sont les gestes inverses de l'installation. Le runner se retire de
l'API avant la révocation du jeton, car c'est ce jeton qui authentifie
l'appel. Le jeton se révoque par un DELETE en authentification Basic comme
dans la leçon
Assister les utilisateurs.
api -X DELETE "$API/repos/fjadmin/l8test" -o /dev/null -w 'delete repo HTTP %{http_code}\n'api -X DELETE "$API/repos/fjadmin/l8eph" -o /dev/null -w 'delete repo HTTP %{http_code}\n'sudo systemctl disable --now forgejo-runner.servicesudo rm -f /etc/systemd/system/forgejo-runner.servicesudo systemctl daemon-reloadfor ID in $(api "$API/admin/actions/runners" | python3 -c 'import sys,jsond=json.load(sys.stdin); r=d["runners"] if isinstance(d,dict) else dprint(" ".join(str(x["id"]) for x in r))'); do api -X DELETE "$API/admin/actions/runners/$ID" -o /dev/null -w "delete runner $ID HTTP %{http_code}\n"donePW=$(sudo grep '^ADMIN_PASSWORD=' /opt/forgejo-lab/.env | cut -d= -f2-)curl -sS -u "fjadmin:$PW" -X DELETE "$API/users/fjadmin/tokens/l8tok" -o /dev/null -w 'delete token HTTP %{http_code}\n'; unset PWsudo rm -f /tmp/l2.zst /tmp/e.yml /tmp/host-proof.txt /tmp/forge-lab-ca.crtsudo rm -rf /tmp/wfsudo userdel -r runnersudo rm -f /usr/local/bin/forgejo-runnersudo rm -rf /etc/forgejo-runnersudo rm -f /etc/pki/ca-trust/source/anchors/forge-lab-ca.crtsudo update-ca-trustLes quatre suppressions par l'API (deux dépôts, le runner, le jeton) répondent
HTTP 204. La boucle retire tous les runners de l'instance : sur une
forge qui en héberge d'autres, filtrez sur le nom ou l'id lu dans la liste.
userdel -r runner supprime le compte et son répertoire, donc le
config.yml et le secret. Vérifiez : id runner répond no such user,
systemctl is-active forgejo-runner répond inactive, l'API ne liste plus
aucun runner, aucun dépôt, aucun jeton, et un curl HTTPS sur la forge répond
de nouveau 000 puisque la CA n'est plus dans le magasin. Ne retirez la CA
que si vous ne gardez pas la forge sur cette VM.
À retenir
Section intitulée « À retenir »- Le binaire du runner se vérifie par
sha256sum -cet pargpg --verify; le messagenot certified with a trusted signatureest normal, la clé primaire (EB114F5E…) se compare à la documentation, et se récupère surkeys.openpgp.org. forgejo-runner registeretcreate-runner-filesont dépréciés en 13.2 : on génère un secret avecforgejo forgejo-cli actions generate-secretet on le déclare avecactions register --secret-stdin. L'UUID dérive du secret, le--namen'est pas retenu.- En mode démon, la configuration porte
server.connections.<nom>avecurl,uuid,tokenetlabels; il n'y a plus de fichier.runner. Le runner doit faire confiance à la CA de la forge. - Un chemin monté par
container.optionsdoit figurer danscontainer.valid_volumes, sinonis not a valid volume, will be ignored. - L'exécuteur
hostn'isole rien : le job lit leconfig.ymldu runner et liste les conteneurs de la forge. Hors lab, on le refuse. - L'exécuteur
dockerne donne pas de socket (docker_host: "-"), mais le job estrootdans son conteneur et son réseau joint la VM. Donner le socket équivaut à donnerhost. - Le masquage
***ne vaut que pour la valeur exacte d'un secret. - Forgejo Actions n'est pas compatible GitHub Actions :
.forgejo/workflows/prime sur.github/workflows/,uses:se résout surdata.forgejo.org, unruns-onsans runner attend sans message, et les variablesFORGEJO_*s'ajoutent. - Le mode éphémère (
register --ephemeral, puisforgejo-runner one-joblancé depuis/home/runner) prend un seul job puis disparaît de l'API ; un éphémère jamais utilisé reste enregistré et se supprime par l'API.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Six questions tirées sur huit : l'enregistrement sans commande dépréciée, les volumes autorisés, ce que peut un job host, le cloisonnement d'un job docker, les écarts avec GitHub Actions et le mode éphémère.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Sauvegarder, restaurer et documenter la reprise
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications