GitHub Actions erreicht Ihr VPN nicht: selbst gehostet oder Tunnel
Ein gehosteter Runner ist eine frische VM außerhalb Ihres Netzes. Setzen Sie entweder einen Runner in dieses Netz, oder bauen Sie einen Tunnel aus dem Job und warten Sie auf eine echte Route.
Ihr Deploy-Schritt hängt an einem internen Hostnamen, während derselbe Befehl von Ihrem Laptop aus sofort antwortet, und der Client-Build auf beiden Rechnern identisch ist. Der Job läuft auf einem GitHub-gehosteten Runner: einer virtuellen Maschine, die für genau diesen einen Job in der GitHub-Cloud erstellt wurde, ohne Route in Ihren privaten Adressraum, ohne Resolver-Eintrag für die interne Zone und ohne Zertifikat, das der private Endpunkt akzeptieren würde. Nur zwei Entwürfe passen zu dieser Randbedingung, und keiner lässt sich später aktivieren, ohne den Workflow neu zu bauen. Entscheiden Sie sich vor der ersten YAML-Zeile, denn die Migration im Nachhinein kostet mehr als der erste Versuch.
Die Wahl liegt zwischen einem Runner, der dort steht, wo das Netzwerk bereits endet, und einem Tunnel, der für die Dauer eines Jobs existiert. Beide tragen seit Jahren Produktions-Deploys, und sie scheitern auf unterschiedliche Weise.
Wo die Grenze tatsächlich verläuft
Alles, was Sie auf Ihrem Laptop installieren, endet an Ihrem Laptop, und alles, was Sie während eines Jobs installieren, verschwindet mit dem Runner. Es gibt keinen persistenten Netzwerk-Namespace, an dem ein Client zwischen den Läufen hängen bleiben könnte, also beginnt jeder Lauf im selben unerreichbaren Zustand. Azure dokumentiert diese Grenze für von Microsoft gehostete Agents, bei denen VPN- und ExpressRoute-Verbindungen in private Netze nicht unterstützt werden, und GitHub-gehostete Runner tragen dieselbe Form von Einschränkung statt eines eigenen Mechanismus. Lesen Sie das als Produkteigenschaft, nicht als Defekt, den man umgehen müsste.
Keine Repository-Einstellung lockert das. Das Job-Label entscheidet, welcher Pool die Arbeit aufnimmt, und ein gehostetes Label bedeutet für den gesamten Lauf eine Maschine außerhalb Ihres Perimeters.
Private Erreichbarkeit muss daher aus etwas kommen, das Ihnen gehört. Ein Runner, der dort steht, wo das Netzwerk bereits endet, oder ein Tunnel, der für einen Job lebt. Der Rest dieses Artikels handelt davon, bewusst für eines davon zu zahlen, statt die Rechnung in der Produktion zu entdecken.
Nach Laufhäufigkeit entscheiden, nicht nach Vertrautheit
Greifen Sie zu einem selbst gehosteten Runner im VPC oder im Büronetz, wenn Deploys mehrmals täglich landen, wenn der Job mehr als einen privaten Port berührt oder wenn ein Plattform-Team bereits Patching und Credential-Rotation verantwortet. Ein Job auf dieser Maschine löst auf und routet wie jeder andere Host hinter der Firewall. Die Netzwerkarbeit passiert einmal, zur Installationszeit, statt bei jedem Lauf.
Bauen Sie den Tunnel im Job, wenn der Zugriff gelegentlich ist und Sie keinen langlebigen Agenten wollen, der eine Zugangsdaten in Ihr Netz hält. Der Job wird für ein paar Minuten sein eigener Netzwerk-Client und lässt nichts zurück, was oft der ganze Punkt ist.
Schätzen Sie die Kosten des Tunnels, bevor Sie ihn wählen. Jeder Lauf zahlt für das Materialisieren des Profils, den Client-Start, die Bereitschaft und den Abbau. Ein Tunnel, der hochkommt, aber nie verifiziert wird, erzeugt ein Deploy, das auf halbem Weg scheitert und privaten Zustand in einer Form hinterlässt, die niemand geplant hat. Der Wiederholungsversuch danach ist selten sauber, und der zweite Fehlschlag wird meist der Client-Version angelastet.
Das Profil so schreiben, dass der Schritt nicht abbrechen kann
Halten Sie Profile und Zugangsdaten aus dem Repository heraus und materialisieren Sie sie aus Secrets in dem Schritt, der sie braucht. Die Reihenfolge entscheidet, ob der Job überlebt: Eine unter sudo erzeugte Datei gehört root, und die unprivilegierte Shell des Schritts kann dann nicht mehr hineinschreiben.
- 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"Beide Details in diesem Block existieren für spätere Schritte. Mit umask 077 landet das Profil bei Modus 0600 im Besitz des Runner-Benutzers, sudo kann es weiterhin lesen, und der Client schreibt sein Log in eine Datei, die dem Runner bereits gehört, sodass die Bereitschaftsschleife dieses Log ohne ein zweites sudo lesen kann. --writepid zeichnet die PID auf, auf die der Abbau zielen wird.
Ein Profil, das sich mit einem Client-Zertifikat authentifiziert, braucht seinen Schlüssel genauso geschrieben, und ein Static-Challenge-Profil braucht den passenden Auth-Modus. Profile, die DNS-Optionen pushen, brauchen openvpn-systemd-resolved auf Ubuntu und Debian, weil der Push allein auf einem Basis-Image nichts ändert.
Bereitschaft ist eine Probe, keine Pause
Ein festes Sleep hält genau so lange, bis der Konzentrator langsam ist, und dann läuft der Deploy-Schritt, bevor die Route existiert. Der Fehler taucht dann im Deploy-Werkzeug als Timeout gegen den privaten Host auf, was nichts über den Tunnel aussagt.
- 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 1Die Schleife beobachtet zwei Dinge, weil jedes davon die kaputte Hälfte sein kann. getent hosts beweist, dass der gepushte Resolver für diesen Namen antwortet, und das Öffnen des Sockets beweist, dass auf dem Port, den das Deploy nutzen wird, etwas lauscht; ein reines Routen-Profil braucht die wörtliche Adresse, und die Zeile getent muss gelöscht werden.
Wenn die Schleife abläuft, ist das Log das Einzige, was drei verschiedene Fehler trennt: ein abgelehntes Zertifikat, ein falscher Auth-Modus und eine Route, die nie ankam. Vor dem tail zu filtern hält diese Ausgabe kurz genug zum Lesen, während der Job noch scheitert. Lesen Sie den ersten Fehler statt der letzten Zeile, denn ein Client, der sich nicht authentifizieren kann, wiederholt sich, bis die Schleife aufgibt. ip -4 route show dev tun0 beantwortet die einfachere Frage, ob überhaupt eine Route gepusht wurde, und kostet eine Zeile in derselben Schleife.
Abbau und die Frage nach dem Artefakt
Schließen Sie den Job mit einem Schritt, der durch if: always() geschützt ist und sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" und sonst nichts ausführt. Auf einem geteilten selbst gehosteten Runner gilt: niemals nach Prozessnamen töten: Ein anderer Job kann einen Client mit demselben Binary und ähnlichen Argumenten besitzen. Der Fehler landet dann bei dem Job, der als zweites lief, und dessen Log auf einen Workflow zeigt, der nichts falsch gemacht hat.
Die Absicherung zählt, weil ein fehlgeschlagenes Deploy trotzdem einen laufenden Tunnel hinterlässt. Ohne always() wird der Aufräumschritt genau dann übersprungen, wenn er gebraucht wird, und der nächste Lauf startet gegen einen Client, der bereits verbunden ist.
Eine Frage bleibt, bevor Sie einen der beiden Entwürfe übernehmen: Braucht die Build-Stufe überhaupt eine private Route? Ein Deployment-Agent, der bereits im Netz sitzt, kann ein genehmigtes Artefakt ziehen, was gewöhnliche Builds vom privaten Pfad fernhält. Weniger Jobs, die einen Tunnel halten, bedeuten weniger Stellen, an denen sich eine veraltete Route verstecken kann.