Ein GitHub-Actions-Job ist ein guter Ort, um Secrets zu stehlen. Er enthält Deploy Keys, Registry-Tokens und Cloud-Zugangsdaten, und er führt Code aus, den vor einer Minute jemand anderes geschrieben hat: Actions von Drittanbietern, Pakete und ihre Install-Skripte. GitHubs Umgang mit Secrets schützt vor Versehen. Er wurde nicht dafür entworfen, Code aufzuhalten, der bereits im Job läuft.

Dieser Beitrag erklärt, wie Secrets in GitHub Actions funktionieren, auf welchen fünf Wegen sie in der Praxis abfließen und welche Kontrollen jeden davon schließen.

Wie GitHub Actions Secrets funktionieren

Secrets gibt es auf drei Ebenen: Organisation, Repository und Environment. Existiert derselbe Name auf mehreren Ebenen, gewinnt die spezifischste: Environment vor Repository, Repository vor Organisation.

Einige Grenzen und Regeln, die Sie kennen sollten:

  • Größe und Anzahl. Bis zu 1.000 Organisations-Secrets, 100 Repository-Secrets und 100 Environment-Secrets, jedes höchstens 48 KB groß. Ein Workflow sieht jedes Repository- und Environment-Secret, aber nur die ersten 100 Organisations-Secrets in alphabetischer Reihenfolge.
  • Forks bekommen nichts. Abgesehen von GITHUB_TOKEN werden Secrets nicht an Workflows weitergegeben, die durch einen Pull Request aus einem Fork ausgelöst werden.
  • Dependabot wird wie ein Fork behandelt. Von Dependabot ausgelöste Workflows erhalten ein Read-only-Token und nur Dependabot-Secrets, keine Actions-Secrets.
  • Wiederverwendbaren Workflows müssen Secrets übergeben werden. secrets: inherit übergibt alle implizit, innerhalb derselben Organisation oder Enterprise. Environment-Secrets lassen sich nicht über workflow_call übergeben.
  • Environments regeln den Zugriff. Mit Required Reviewers kann ein Job die Secrets eines Environments erst lesen, wenn jemand ihn freigibt. Deployment-Branch-Regeln begrenzen, welche Branches das Environment überhaupt nutzen können.

Was Masking leistet und was nicht

GitHub entfernt Secret-Werte aus Logs, aber diese Schwärzung setzt voraus, dass eine exakte Übereinstimmung gefunden wird. GitHubs eigene Dokumentation nennt, wo das scheitert:

  • ein Secret in strukturierten Daten wie JSON oder YAML
  • ein von einem Secret abgeleiteter Wert, etwa ein Token, das gegen ein anderes Token getauscht wurde, sofern Sie ihn nicht mit ::add-mask:: registrieren
  • ein transformiertes Secret, zum Beispiel Base64-kodiert

Masking schützt davor, ein Secret versehentlich auszugeben. Gegen Code, der das Secret vorher kodiert, hilft es nicht.

Wie Secrets aus Workflows abfließen

1. Eine kompromittierte Action gibt sie aus

Im März 2025 kompromittierte ein Angreifer tj-actions/changed-files (CVE-2025-30066), eine Action, die von mehr als 23.000 Repositories genutzt wurde. Er verschob die vorhandenen Versions-Tags der Action auf einen bösartigen Commit, sodass jeder Workflow, der einen Tag wie @v45 referenzierte, den Code des Angreifers ausführte. Sicher waren nur Workflows, die auf einen vollständigen Commit-SHA gepinnt waren.

Die Schadsoftware las Secrets aus dem Speicher des Runners und schrieb sie ins Job-Log, doppelt Base64-kodiert, damit das Masking sie nicht erfasste. Bei öffentlichen Repositories kann jeder diese Logs lesen. Die Kompromittierung begann bei einer anderen Action, reviewdog/action-setup (CVE-2025-30154), und davor bei einem Personal Access Token, das über einen pull_request_target-Workflow gestohlen wurde.

2. Ein pull_request_target-Workflow führt nicht vertrauenswürdigen Code aus

pull_request_target läuft im Kontext des Basis-Repositorys, mit dessen Secrets und einem schreibberechtigten Token, auch wenn der Pull Request aus einem Fork stammt. Das ist nur sicher, solange der Workflow keinen Code aus dem Pull Request ausführt. Checkt ein Workflow den Head des Pull Requests aus und baut ihn, kann jeder, der einen Pull Request öffnen kann, jedes Secret des Workflows lesen. Das GitHub Security Lab nennt das „pwn requests“.

GitHub schränkt das zunehmend ein. Seit Dezember 2025 verwendet pull_request_target immer die Workflow-Datei aus dem Default-Branch. Seit Juli 2026 weigert sich actions/checkout, Fork-Code in pull_request_target-Workflows auszuchecken, die bekannte gefährliche Muster verwenden, und die seit September 2026 allgemein verfügbaren Workflow Execution Protections werden pull_request_target ab dem 2. November 2026 in öffentlichen Repositories ohne Event-Policy standardmäßig deaktivieren.

3. Nicht vertrauenswürdige Eingaben werden in ein Skript injiziert

Workflow-Ausdrücke werden expandiert, bevor die Shell läuft. Ein Schritt wie dieser:

- run: echo "Title: ${{ github.event.issue.title }}"

führt aus, was auch immer im Issue-Titel steht. Ein Angreifer öffnet ein Issue mit einem Shell-Befehl als Titel, und der Job führt ihn aus, mit den Secrets des Jobs in Reichweite. Übergeben Sie nicht vertrauenswürdige Werte stattdessen über eine Umgebungsvariable, damit die Shell sie als Daten behandelt:

- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "Title: $TITLE"

4. Caches und Artefakte tragen sie nach draußen

  • Artefakte. 2024 zeigte die ArtiPACKED-Forschung von Unit 42, dass actions/checkout standardmäßig GITHUB_TOKEN in .git/config schreibt. Wird der Workspace als Artefakt hochgeladen, gelangt das Token an jeden, der das Artefakt herunterladen kann. upload-artifact schließt seit September 2024 versteckte Dateien standardmäßig aus, und actions/checkout v6 speichert die Zugangsdaten außerhalb des Checkout-Verzeichnisses, aber ältere gepinnte Versionen und eigene Upload-Schritte lassen das Token weiterhin abfließen.
  • Caches. Ein Angreifer, der in irgendeinem Workflow eines Repositorys Code ausführen kann, kann dessen Cache füllen, die echten Einträge verdrängen und unter demselben Schlüssel einen vergifteten Eintrag platzieren. Ein späterer privilegierter Workflow stellt ihn wieder her. Im Mai 2026 ermöglichte diese Kette, die bei pull_request_target begann und mit einem aus dem Runner-Speicher gelesenen OIDC-Token endete, einem Angreifer, 84 bösartige Versionen von 42 TanStack-Paketen zu veröffentlichen.

5. Eine Abhängigkeit sendet sie an einen Server

Install-Skripte laufen mit denselben Zugriffsrechten wie der Rest des Jobs. Der Wurm Shai-Hulud, erstmals im September 2025 auf npm gesehen, lief während npm install, sammelte npm-Tokens, GitHub-Tokens und Cloud-Schlüssel aus der Umgebung und veröffentlichte sie in öffentlichen GitHub-Repositories. Seine Welle vom November 2025 lief beim Preinstall und registrierte den infizierten Rechner als selbst gehosteten Runner, sodass der Angreifer später zurückkommen konnte.

Dieser Weg erfordert keinen Fehler in Ihrem Workflow. Eine einzige kompromittierte Version irgendwo im Abhängigkeitsbaum genügt, und die Secrets verlassen den Job über das Netzwerk.

So sichern Sie sie ab

Ersetzen Sie gespeicherte Cloud-Schlüssel durch OIDC. Statt einen AWS-, Azure- oder Google-Cloud-Schlüssel als Secret zu hinterlegen, lassen Sie den Job ein kurzlebiges Token beim OIDC-Issuer von GitHub anfordern und beim Cloud-Anbieter eintauschen. Die Trust Policy legt fest, welches Repository, welcher Branch oder welches Environment die Rolle übernehmen darf, und die Zugangsdaten laufen mit dem Job ab. Der Job braucht permissions: id-token: write. OIDC-Tokens lassen sich aus einem laufenden Job weiterhin stehlen, wie TanStack gezeigt hat, aber ein gestohlenes Token ist weit weniger wert als ein Schlüssel, der nie abläuft.

Pinnen Sie Actions auf einen vollständigen Commit-SHA. GitHubs Dokumentation nennt das den einzigen Weg, eine Action als unveränderliches Release zu nutzen. Tags lassen sich verschieben, ein SHA nicht. Seit August 2025 kann die Allowed-Actions-Policy SHA-Pinning erzwingen und bestimmte Actions in einer ganzen Organisation blockieren. Dependabot und Renovate können gepinnte SHAs aktuell halten.

Geben Sie GITHUB_TOKEN nur so viel, wie es braucht. Setzen Sie den Standard auf Read-only und vergeben Sie Schreibrechte pro Job. Neue Organisationen und Repositories sind seit Februar 2023 standardmäßig Read-only, ältere behielten aber die großzügige Voreinstellung, sofern niemand sie geändert hat. Siehe GITHUB_TOKEN-Berechtigungen: Least Privilege für jeden Workflow.

Beschränken Sie Secrets auf den Schritt, der sie braucht. Ein Secret im env: eines Schritts befindet sich nur in der Umgebung dieses Schritts. Ein auf Job-Ebene gesetztes Secret ist für jeden Schritt sichtbar, also auch für jede Action von Drittanbietern.

Legen Sie Produktions-Secrets hinter Environments. Required Reviewers und Deployment-Branch-Regeln sorgen dafür, dass ein Workflow auf einem Feature-Branch oder einer, der durch einen Pull Request ausgelöst wurde, sie nicht lesen kann.

Behandeln Sie pull_request_target und nicht vertrauenswürdige Eingaben als Codeausführung. Vermeiden Sie pull_request_target, sofern Sie es nicht brauchen, bauen Sie darin nie Pull-Request-Code, und übergeben Sie Event-Daten über Umgebungsvariablen. CodeQL kann Workflows auf Injection- und Pwn-Request-Muster prüfen und ist seit April 2025 für GitHub Actions allgemein verfügbar.

Aktivieren Sie Secret Scanning und Push Protection. Sie erkennen Secrets, die ins Repository committet wurden. Das ist ein anderes Leck als die oben genannten, aber ebenso verbreitet.

Beschränken Sie, wohin sich der Job verbinden kann. Die meisten Jobs brauchen eine kurze Liste von Zielen: Paket-Registries, Container-Registries und einige APIs. Kann der Job nur diese erreichen, kann ein von Schadcode gelesenes Secret nirgendwohin. Siehe wie eine Egress-Richtlinie die Exfiltration aus GitHub Actions stoppt.

Rotieren Sie nach jeder Exposition. Hat ein Workflow eine kompromittierte Action oder ein kompromittiertes Paket ausgeführt, gehen Sie davon aus, dass jedes Secret, das er lesen konnte, gestohlen wurde. Widerrufen Sie alle und stellen Sie sie neu aus, nicht nur die, von denen Sie annehmen, dass sie verwendet wurden.

Wo CRACI ins Spiel kommt

CRACI führt Ihre GitHub-Actions-Jobs auf eigenen Runnern aus: Sie ändern runs-on auf craci, und die Workflows, die Secrets und die Laufhistorie bleiben in GitHub. Zwei seiner Funktionen zielen auf die oben genannten Lecks.

  • Eine Egress-Richtlinie, die im Zweifel blockiert. Sie legen fest, welche Paketquellen, Registries und Hosts ein Job erreichen darf. Die Richtlinie wird vor dem Start des Jobs validiert und auf dem Runner durchgesetzt, sodass eine Verbindung zu jedem anderen Ziel blockiert wird, und Sie erhalten einen Alarm, wenn gegen die Richtlinie verstoßen wird.
  • Eine Aufzeichnung dessen, was der Job getan hat. CRACI zeichnet jedes Paket auf, das jeder Job tatsächlich geladen hat, einschließlich aus Caches wiederhergestellter Pakete, sowie einen Trace der Netzwerkverbindungen des Jobs. Wird das nächste kompromittierte Paket bekannt, sehen Sie, welche Builds es geladen haben und wohin sie sich verbunden haben, statt anhand von Lockfiles zu raten.

Jeder Job läuft in seiner eigenen virtuellen Maschine, sodass nichts, was ein Job hinterlässt, für den nächsten vorhanden ist. CRACI scannt Ihren Code nicht auf Secrets; dafür behalten Sie das Secret Scanning von GitHub.

Buchen Sie eine Demo, um die Aufzeichnung eines Ihrer eigenen Builds zu sehen.