GitHub-Actions-Secrets geleakt: Exfiltration untersuchen
Wie GitHub-Actions-Secrets trotz ***-Maskierung leaken: manipulierte Workflows, neue Branches, kodierte Ausgaben, fremde Actions – und welche Beweise zählen.
Kurz gesagt. Ein Workflow kann jedes Secret in seinem Geltungsbereich lesen, und die Maskierung verbirgt nur den exakten Wert. echo "$KEY" | base64 | base64 gibt ihn im Klartext aus. Angreifer kommen dorthin, indem sie einen veränderten Workflow auf einen neuen Branch pushen, pull_request_target missbrauchen oder eine Action eines Drittanbieters kompromittieren (tj-actions/changed-files, 2025). Die Beweise: workflows.created_workflow_run auf einem nie gesehenen Branch im Audit-Log, dazu das Protokollarchiv des Laufs, in dem sowohl das Skript des Schritts als auch die kodierte Zeichenkette sichtbar sind. Laden Sie die Archive herunter, bevor jemand sie löscht, und rotieren Sie dann jedes Secret, das der Workflow lesen konnte.
In GitHub Actions wird aus Quellcode Cloud-Zugriff. Die Workflows, die in die Produktion deployen, halten AWS-Schlüssel, Registry-Tokens und Signaturschlüssel als Secrets. Für einen Angreifer mit einem Token mit workflow-Scope oder einem Brückenkopf in einer Action, von der Sie abhängen, ist die Exfiltration dieser Secrets oft das eigentliche Ziel.
Wie Secrets zum Angreifer gelangen
| Weg | Was passiert | Wo es sichtbar wird |
|---|---|---|
| Workflow auf einen neuen Branch gepusht | Der Angreifer pusht eine veränderte .github/workflows/*.yml auf einen frischen Branch. Ein push-Trigger führt sie mit den Secrets des Repositorys aus | git.push + workflows.created_workflow_run mit neuem head_branch |
Missbrauch von pull_request_target / workflow_run | Nicht vertrauenswürdiger PR-Code läuft in einem privilegierten Kontext | Läufe, ausgelöst durch PRs aus Forks, siehe GitHub Security Lab zu „pwn requests“ |
| Kompromittierte Action eines Drittanbieters | Ein von Ihnen genutzter Tag zeigt jetzt auf bösartigen Code | Jeder Lauf, der den Tag verwendet hat, mit derselben seltsamen Ausgabe |
| Selbst gehosteter Runner | Secrets und Tokens werden von einem persistenten Host gelesen | Siehe Sicherheit selbst gehosteter Runner |
| Schutz der Umgebung entfernt | Erforderliche Reviewer oder Branch-Richtlinien gelöscht, sodass Umgebungs-Secrets erreichbar werden | environment.remove_protection_rule, environment.delete |
ATT&CK hat für diese Familie inzwischen eine eigene Technik, T1677 Poisoned Pipeline Execution. Der Glossareintrag zu Poisoned Pipeline Execution erklärt die direkte und die indirekte Variante.
Ein öffentliches Beispiel
Im März 2025 veröffentlichte die CISA eine Warnung zur Kompromittierung von tj-actions/changed-files (CVE-2025-30066) und des damit zusammenhängenden reviewdog/action-setup (CVE-2025-30154). Versions-Tags der Action waren so umgebogen worden, dass sie auf bösartigen Code zeigten, der CI-Secrets in die Workflow-Protokolle schrieb. In öffentlichen Repositories kann jeder diese Protokolle lesen. Jede Organisation, die einen verschobenen Tag nutzte, musste ihre Laufprotokolle für den betroffenen Zeitraum prüfen. Die Erkennungsfrage lautete: Welche unserer Läufe haben in diesem Schritt eine kodierte Zeichenkette ausgegeben?
Warum Maskierung keine Schutzmaßnahme ist
GitHub ersetzt registrierte Secret-Werte in Protokollen durch ***. Die Referenz zur sicheren Nutzung ist eindeutig: Die Schwärzung ist nicht garantiert, und ein umgewandeltes Secret (Base64, URL-kodiert) muss separat registriert werden, damit es maskiert wird. Deshalb gibt
echo "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" | base64 -w0 | base64 -w0
eine lange Zeichenkette aus, zu der keine Maske passt. Zweimal dekodiert, haben Sie das Schlüsselpaar. Varianten sind xxd, rev, od, das Aufteilen des Werts, toJSON(secrets) oder das direkte Versenden des Werts mit curl -d an einen Dienst zum Mitschneiden von Requests. Der Glossareintrag zur Secret-Maskierung fasst zusammen, was die Maskierung abdeckt.
Die Beweise, nach Aussagekraft geordnet
1. Das Protokollarchiv des Laufs
Repository → Actions → Lauf → ⋯ → Download log archive. Das ZIP enthält ein zusammengefasstes Protokoll pro Job und eine Datei pro Schritt. Darin stehen das Skript des Schritts (der Block ##[group]Run … gibt die Befehle wieder) und seine Ausgabe. Nur hier sehen Sie, was der Workflow mit einem Secret getan hat. Protokolle werden standardmäßig 90 Tage aufbewahrt, und jeder mit Schreibzugriff kann sie löschen. Laden Sie sie zuerst herunter.
Der Analyzer liest diese ZIPs direkt. Zwei Regeln gelten für sie:
actions-secret-exfil(kritisch): eine Skriptzeile, die ein Secret, Token, einen Schlüssel, env oder eine$-Variable erwähnt und sie durch einen Encoder leitet (base64,xxd,rev,od), die Umgebung ausgibt (printenv,env |,toJSON(secrets)) oder Daten an einen externen Endpunkt sendet (curl -d,wget --post,/dev/tcp/, gängige Dienste zum Mitschneiden von Requests und für Tunnel). Reines Dekodieren (base64 -d) ohne Netzwerkaufruf ist ausgenommen, um Fehlalarme zu begrenzen. Nennt die Zeile Zugangsdaten für AWS, Google Cloud oder Azure, zeigt der Befund den nächsten Schritt in der Cloud an.actions-encoded-output(mittel): eine Ausgabezeile mit einer durchgehenden Base64- oder Hex-Zeichenkette von 60 oder mehr Zeichen. Genau so sieht ein doppelt kodierter Schlüssel aus.
Jede Protokollzeile kommt in einem Archiv zweimal vor (in der zusammengefassten Datei und in der Datei pro Schritt). Der Analyzer dedupliziert, sodass ein bösartiger Schritt genau einen Befund ergibt.
2. Die Workflow-Lauf-Ereignisse im Audit-Log
workflows.created_workflow_run und workflows.completed_workflow_run enthalten repo, name (den Workflow), event (push, pull_request, workflow_dispatch …), head_branch, head_sha, workflow_run_id und den Akteur. Die Regel workflow-new-branch (niedrig) markiert einen Workflow, der auf einem Branch läuft, auf dem er noch nie gelaufen ist, und zwar für die Trigger push und workflow_dispatch. Für sich allein ist sie laut, da jeder Feature-Branch sie auslöst. Bedeutsam wird sie, wenn der Akteur oder das Token weitere Befunde hat oder wenn der Branch-Name nach Wartung klingt (ci/cache-fix, dependabot-fix) und der Branch danach gelöscht wurde.
3. Git-Events
Ein git.push kurz vor dem Lauf, vom selben Token und derselben IP-Adresse, verknüpft die Workflow-Änderung mit den Zugangsdaten. Ohne Git-Events bleibt Ihnen head_sha, den Sie im Repository nachschlagen können, sofern der Branch noch existiert.
4. Änderungen an Secret-Metadaten
repo.create_actions_secret, org.update_actions_secret und environment.create_actions_secret (Regel actions-secret-created, niedrig) sind routinemäßige CI-Arbeit. Relevant werden sie, wenn der Akteur verdächtig ist. Ein Angreifer kann ein Deployment-Secret ersetzen, um Artefakte umzuleiten. Beachten Sie, was das Audit-Log nicht protokolliert: Dass ein Workflow ein Secret liest, ist kein Ereignis.
Umfang bestimmen und Maßnahmen
- Listen Sie jeden Lauf des veränderten Workflows und jedes Workflows auf den Branches des Angreifers auf, mit den Werten von
workflow_run_idaus dem Audit-Log. - Rotieren Sie jedes Secret, das der Workflow lesen konnte: Secrets des Repositorys, der Organisation (sofern das Repository Zugriff hat) und der Umgebungen. Auch diejenigen, die nicht ausgegeben wurden. Ein Skript, das
envausführt, verschickt alles. - Löschen Sie die Protokolle der Läufe, die Secrets ausgegeben haben – nachdem Sie sie für die Fallakte archiviert haben –, damit die kodierten Zeichenketten in der Oberfläche nicht mehr lesbar sind.
- Entfernen Sie die Branches des Angreifers und prüfen Sie
.github/workflowsauf allen verbliebenen Branches. - Verfolgen Sie die Schlüssel in AWS, Google Cloud oder Azure: siehe geleakte Cloud-Schlüssel.
Härtung, die die Angriffsfläche tatsächlich verkleinert
- Pinnen Sie Actions von Drittanbietern auf einen vollständigen Commit-SHA. GitHub nennt das „die einzige Möglichkeit, eine Action als unveränderliches Release zu verwenden“.
- Setzen Sie die Standardberechtigung des
GITHUB_TOKENauf Nur-Lesen und vergeben Sie Schreibrechte pro Job. - Schützen Sie Deployment-Secrets mit Umgebungen, die Reviewer verlangen und Branches einschränken. Der Analyzer markiert, wenn dieser Schutz entfernt wird (
environment-protection-removed, hoch). - Ersetzen Sie gespeicherte Cloud-Schlüssel durch OIDC-Federation. Ein kurzlebiges Token, das auf ein Repository und einen Branch beschränkt ist, ist für einen Angreifer weit weniger wert.
- Verlangen Sie ein CODEOWNERS-Review für
.github/workflows/und schützen Sie das Verzeichnis mit einem Repository-Ruleset. - Nutzen Sie die Prüfungen von OpenSSF Scorecard (Token-Berechtigungen, gepinnte Abhängigkeiten, gefährliche Workflows), um riskante Muster zu finden.
FAQ
Können GitHub-Actions-Secrets leaken, obwohl sie als *** maskiert werden?
Ja. Die Maskierung ersetzt in Protokollen nur den exakten Wert des Secrets. Ein Skript, das ihn Base64-kodiert, umkehrt oder aufteilt, gibt eine Zeichenkette aus, die nicht mehr übereinstimmt – sie erscheint also im Klartext und lässt sich dekodieren. GitHubs eigene Dokumentation warnt, dass die Schwärzung bei umgewandelten Secrets nicht garantiert ist.
Zeigt das Audit-Log, wann ein Workflow ein Secret liest?
Nein. Das Audit-Log protokolliert, wenn Secrets angelegt, geändert oder entfernt werden und wenn Workflow-Läufe erstellt und abgeschlossen werden. Das Lesen eines Secrets während eines Laufs ist kein Audit-Ereignis; was ein Schritt damit getan hat, belegt daher nur das Laufprotokoll.
Verwandte Artikel
- Geleakte Cloud-Schlüssel in GitHub Actions: der Weg in die Cloud
- Branch-Schutz deaktiviert? GitHub-Rulesets prüfen
- Die fiktive Fallstudie northwind-labs: ein doppelt Base64-kodierter AWS-Schlüssel in einem Laufprotokoll
- OWASP Top 10 CI/CD Security Risks