GitHub Actions n’atteint pas votre VPN : runner auto-hébergé ou tunnel
Un runner hébergé est une VM neuve hors de votre réseau. Soit vous placez un runner dans ce réseau, soit vous tunnelisez depuis le job et attendez une vraie route.
Votre étape de déploiement se bloque sur un nom d’hôte interne alors que la même commande répond instantanément depuis votre portable, et la compilation du client est identique sur les deux machines. Le job tourne sur un runner hébergé par GitHub : une machine virtuelle créée pour ce seul job dans le cloud de GitHub, sans route vers votre espace d’adressage privé, sans entrée de résolveur pour la zone interne et sans certificat que le point de terminaison privé acceptera. Deux conceptions seulement tiennent cette contrainte, et aucune ne peut être activée plus tard sans reconstruire le workflow. Choisissez avant la première ligne de YAML, car migrer après coup coûte plus cher que la première tentative.
Le choix se joue entre un runner placé là où le réseau s’arrête déjà et un tunnel qui n’existe que le temps d’un job. Les deux portent des déploiements de production depuis des années, et ils échouent différemment.
Où se situe réellement la frontière
Tout ce que vous installez sur votre portable s’arrête à votre portable, et tout ce que vous installez pendant un job disparaît avec le runner. Il n’existe aucun espace de noms réseau persistant auquel un client resterait attaché entre deux exécutions, donc chaque exécution repart du même état inatteignable. Azure documente cette frontière pour les agents hébergés par Microsoft, où les liaisons VPN et ExpressRoute vers des réseaux privés ne sont pas prises en charge, et les runners hébergés par GitHub portent cette même forme de restriction plutôt qu’un mécanisme distinct. Lisez cela comme une propriété du produit, pas comme un défaut à contourner.
Aucun réglage du dépôt ne l’assouplit. Le label du job décide quel pool prend le travail, et un label hébergé signifie une machine hors de votre périmètre pendant toute l’exécution.
L’accessibilité privée doit donc venir de quelque chose que vous possédez. Un runner placé là où le réseau s’arrête déjà, ou un tunnel qui vit le temps d’un job. Le reste de cet article explique comment payer l’une de ces deux options volontairement au lieu de découvrir la facture en production.
Décider par fréquence d’exécution, pas par habitude
Prenez un runner auto-hébergé dans le VPC ou le réseau du bureau lorsque les déploiements arrivent plusieurs fois par jour, lorsque le job touche plus d’un port privé, ou lorsqu’une équipe plateforme gère déjà le patch et la rotation des identifiants. Un job sur cette machine résout et route comme n’importe quel autre hôte derrière le pare-feu. Le travail réseau se fait une fois, à l’installation, au lieu de chaque exécution.
Construisez le tunnel dans le job lorsque l’accès est occasionnel et que vous ne voulez pas d’un agent durable détenant un identifiant vers votre réseau. Le job devient son propre client réseau pendant quelques minutes et ne laisse rien derrière lui, ce qui est souvent tout l’intérêt.
Estimez le coût du tunnel avant de le choisir. Chaque exécution paie la matérialisation du profil, le démarrage du client, l’attente de disponibilité et le démontage. Un tunnel qui monte mais n’est jamais vérifié produit un déploiement qui échoue à mi-chemin et laisse l’état privé dans une forme que personne n’avait prévue. La reprise ensuite est rarement propre, et le second échec est généralement imputé à la version du client.
Écrire le profil pour que l’étape ne puisse pas avorter
Gardez les profils et les identifiants hors du dépôt et matérialisez-les depuis des secrets dans l’étape qui en a besoin. L’ordre décide si le job survit : un fichier créé sous sudo appartient à root, et le shell non privilégié de l’étape ne peut plus y écrire.
- name: Bring up OpenVPN
run: |
umask 077
profile="$RUNNER_TEMP/client.ovpn"
printf '%s\n' "$VPN_PROFILE" > "$profile"
: > "$RUNNER_TEMP/openvpn.log"
printf '%s\n' "$VPN_CREDENTIALS" > "$RUNNER_TEMP/credentials.txt"
sudo openvpn --daemon openvpn-job \
--config "$profile" \
--auth-user-pass "$RUNNER_TEMP/credentials.txt" \
--writepid "$RUNNER_TEMP/openvpn.pid" \
--log "$RUNNER_TEMP/openvpn.log"Les deux détails de ce bloc existent pour les étapes suivantes. Avec umask 077 le profil arrive en mode 0600 et appartient à l’utilisateur du runner, sudo peut encore le lire, et le client écrit son journal dans un fichier que le runner possède déjà, si bien que la boucle de disponibilité peut lire ce journal sans second sudo. --writepid enregistre le PID que le démontage ciblera.
Un profil qui s’authentifie par certificat client a besoin de sa clé écrite de la même façon, et un profil à défi statique a besoin du mode d’authentification correspondant. Les profils qui poussent des options DNS ont besoin de openvpn-systemd-resolved sur Ubuntu et Debian, car la poussée seule ne change rien sur une image de base.
La disponibilité est une sonde, pas une pause
Un sleep fixe tient jusqu’au moment où le concentrateur ralentit, et l’étape de déploiement s’exécute alors avant que la route existe. L’échec remonte ensuite dans l’outil de déploiement sous forme de timeout vers l’hôte privé, ce qui ne dit rien du tunnel.
- name: Await private endpoint
run: |
for attempt in $(seq 1 30); do
if getent hosts private.example.internal > /dev/null && \
timeout 3 bash -c 'exec 3<>/dev/tcp/private.example.internal/8443' 2>/dev/null; then
echo "endpoint answered after $attempt attempts"
exit 0
fi
sleep 2
done
grep -Ei 'error|verify|auth' "$RUNNER_TEMP/openvpn.log" | tail -40
exit 1La boucle observe deux éléments car l’un ou l’autre peut être la moitié cassée. getent hosts prouve que le résolveur poussé répond pour ce nom, et l’ouverture de la socket prouve que quelque chose écoute sur le port que le déploiement utilisera ; un profil à route seule a besoin de l’adresse littérale et de la ligne getent supprimée.
Quand la boucle expire, le journal est la seule chose qui sépare trois pannes différentes : un certificat rejeté, un mode d’authentification erroné et une route jamais arrivée. Filtrer avant le tail garde cette sortie assez courte pour être lue pendant que le job échoue encore. Lisez la première erreur plutôt que la dernière ligne, car un client qui ne peut pas s’authentifier se répète jusqu’à ce que la boucle abandonne. ip -4 route show dev tun0 répond à la question plus simple de savoir si une route a été poussée, et cela coûte une ligne dans la même boucle.
Démontage, et la question de l’artefact
Terminez le job par une étape gardée par if: always() qui exécute sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" et rien d’autre. Sur un runner auto-hébergé partagé, ne tuez jamais par nom de processus : un autre job peut posséder un client avec le même binaire et des arguments similaires. La panne retombe alors sur le job qui a tourné en second, et son journal pointe vers un workflow qui n’a rien fait de mal.
La garde compte parce qu’un déploiement échoué laisse quand même un tunnel vivant. Sans always(), l’étape de nettoyage est ignorée exactement au moment où elle est nécessaire, et l’exécution suivante démarre contre un client déjà connecté.
Une question subsiste avant d’adopter l’une ou l’autre conception : l’étape de compilation a-t-elle besoin d’une route privée ? Un agent de déploiement déjà installé dans le réseau peut récupérer un artefact approuvé, ce qui éloigne les compilations ordinaires du chemin privé. Moins de jobs détenant un tunnel, c’est moins d’endroits où une route périmée peut se cacher.