Een GitHub Actions-job is een goede plek om secrets te stelen. Hij bevat deploy keys, registrytokens en cloudinloggegevens, en hij voert code uit die iemand anders een minuut geleden heeft geschreven: actions van derden, pakketten en hun installatiescripts. De manier waarop GitHub met secrets omgaat, beschermt tegen ongelukken. Die is er niet op ontworpen om code te stoppen die al in de job draait.
Dit bericht behandelt hoe secrets in GitHub Actions werken, de vijf manieren waarop ze in de praktijk lekken, en de maatregelen die elke route afsluiten.
Hoe GitHub Actions-secrets werken
Secrets bestaan op drie niveaus: organisatie, repository en environment. Bestaat dezelfde naam op meer dan één niveau, dan wint de meest specifieke: environment boven repository, repository boven organisatie.
Een paar limieten en regels die de moeite waard zijn om te kennen:
- Grootte en aantal. Tot 1.000 organisatiesecrets, 100 repositorysecrets en 100 environmentsecrets, elk maximaal 48 KB. Een workflow ziet elk repository- en environmentsecret, maar alleen de eerste 100 organisatiesecrets in alfabetische volgorde.
- Forks krijgen niets. Afgezien van
GITHUB_TOKENworden secrets niet doorgegeven aan workflows die worden gestart door een pull request vanuit een fork. - Dependabot wordt behandeld als een fork. Workflows die door Dependabot worden gestart, krijgen een alleen-lezentoken en alleen Dependabot-secrets, geen Actions-secrets.
- Herbruikbare workflows moeten secrets doorgegeven krijgen.
secrets: inheritgeeft ze allemaal impliciet door, binnen dezelfde organisatie of enterprise. Environmentsecrets kunnen niet viaworkflow_callworden doorgegeven. - Environments regelen de toegang. Met verplichte reviewers kan een job de secrets van een environment pas lezen nadat iemand hem heeft goedgekeurd. Regels voor deploymentbranches beperken welke branches de environment überhaupt kunnen gebruiken.
Wat masking wel en niet doet
GitHub maskeert secretwaarden in logs, maar die maskering is afhankelijk van een exacte match. GitHubs eigen documentatie somt op waar het misgaat:
- een secret in gestructureerde gegevens zoals JSON of YAML
- een waarde die is afgeleid van een secret, zoals een token dat is ingewisseld voor een ander token, tenzij u die registreert met
::add-mask:: - een secret dat is getransformeerd, bijvoorbeeld base64-gecodeerd
Masking is een bescherming tegen het per ongeluk printen van een secret. Het doet niets tegen code die het secret eerst codeert.
Hoe secrets uit workflows lekken
1. Een gecompromitteerde action print ze
In maart 2025 compromitteerde een aanvaller tj-actions/changed-files (CVE-2025-30066), een action die door meer dan 23.000 repositories werd gebruikt. Hij verplaatste de bestaande versietags van de action naar een kwaadaardige commit, zodat elke workflow die naar een tag zoals @v45 verwees de code van de aanvaller uitvoerde. Alleen workflows die waren vastgepind op een volledige commit-SHA waren veilig.
De payload las secrets uit het geheugen van de runner en printte ze in het joblog, twee keer base64-gecodeerd zodat masking ze niet zou herkennen. Op openbare repositories zijn die logs voor iedereen leesbaar. De compromittering begon bij een andere action, reviewdog/action-setup (CVE-2025-30154), en daarvoor bij een personal access token dat was gestolen via een pull_request_target-workflow.
2. Een pull_request_target-workflow voert onbetrouwbare code uit
pull_request_target draait in de context van de basisrepository, met de secrets daarvan en een token dat kan schrijven, zelfs als de pull request uit een fork komt. Dat is alleen veilig zolang de workflow nooit code uit de pull request uitvoert. Als een workflow de head van de pull request uitcheckt en bouwt, kan iedereen die een pull request kan openen elk secret van de workflow lezen. GitHub Security Lab noemt dit "pwn requests".
GitHub perkt dit steeds verder in. Sinds december 2025 gebruikt pull_request_target altijd het workflowbestand van de standaardbranch. Sinds juli 2026 weigert actions/checkout forkcode uit te checken in pull_request_target-workflows die bekende gevaarlijke patronen gebruiken, en workflow execution protections, algemeen beschikbaar sinds september 2026, schakelen pull_request_target vanaf 2 november 2026 standaard uit in openbare repositories zonder event policy.
3. Onbetrouwbare invoer wordt in een script geïnjecteerd
Workflow-expressies worden ingevuld voordat de shell draait. Een stap als deze:
- run: echo "Title: ${{ github.event.issue.title }}"
voert uit wat er in de issuetitel staat. Een aanvaller opent een issue met een shellcommando als titel en de job voert het uit, met de secrets van de job binnen bereik. Geef onbetrouwbare waarden in plaats daarvan door via een omgevingsvariabele, zodat de shell ze als gegevens behandelt:
- env:
TITLE: ${{ github.event.issue.title }}
run: echo "Title: $TITLE"
4. Caches en artefacten voeren ze af
- Artefacten. In 2024 toonde het ArtiPACKED-onderzoek van Unit 42 aan dat
actions/checkoutstandaardGITHUB_TOKENnaar.git/configschrijft, zodat het uploaden van de workspace als artefact het token lekt naar iedereen die het kan downloaden.upload-artifactsluit sinds september 2024 standaard verborgen bestanden uit, enactions/checkoutv6 slaat de inloggegevens op buiten de checkoutmap, maar oudere vastgepinde versies en eigen uploadstappen lekken nog steeds. - Caches. Een aanvaller die code kan uitvoeren in welke workflow van een repository dan ook, kan de cache vullen, de echte items eruit duwen en onder dezelfde sleutel een vergiftigd item plaatsen. Een latere workflow met hogere rechten zet dat item terug. In mei 2026 stelde deze keten, die begon bij
pull_request_targeten eindigde met een OIDC-token dat uit het runnergeheugen werd gelezen, een aanvaller in staat 84 kwaadaardige versies van 42 TanStack-pakketten te publiceren.
5. Een afhankelijkheid stuurt ze naar een server
Installatiescripts draaien met dezelfde toegang als de rest van de job. De Shai-Hulud-worm, voor het eerst gezien op npm in september 2025, draaide tijdens npm install, verzamelde npm-tokens, GitHub-tokens en cloudsleutels uit de omgeving en plaatste ze in openbare GitHub-repositories. De golf van november 2025 draaide bij preinstall en registreerde de geïnfecteerde machine als self-hosted runner, zodat de aanvaller later kon terugkomen.
Voor deze route is geen fout in uw workflow nodig. Eén gecompromitteerde versie ergens in de afhankelijkheidsboom is genoeg, en de secrets verdwijnen via het netwerk.
Hoe u ze afschermt
Vervang opgeslagen cloudsleutels door OIDC. In plaats van een AWS-, Azure- of Google Cloud-sleutel als secret te bewaren, laat u de job een kortlevend token aanvragen bij de OIDC-uitgever van GitHub en dat inwisselen bij de cloudprovider. Het vertrouwensbeleid bepaalt welke repository, branch of environment de rol mag aannemen, en de inloggegevens verlopen met de job. De job heeft permissions: id-token: write nodig. OIDC-tokens kunnen nog steeds uit een draaiende job worden gestolen, zoals TanStack liet zien, maar een gestolen token is veel minder waard dan een sleutel die nooit verloopt.
Pin actions vast op een volledige commit-SHA. GitHubs documentatie noemt dit de enige manier om een action als onveranderlijke release te gebruiken. Tags kunnen worden verplaatst; een SHA niet. Sinds augustus 2025 kan het beleid voor toegestane actions SHA-pinning afdwingen en specifieke actions in een hele organisatie blokkeren. Dependabot en Renovate kunnen vastgepinde SHA's up-to-date houden.
Geef GITHUB_TOKEN het minimum dat nodig is. Stel de standaard in op alleen-lezen en ken schrijfscopes per job toe. Nieuwe organisaties en repositories staan sinds februari 2023 standaard op alleen-lezen, maar oudere hielden de ruime standaard tenzij iemand die wijzigde. Zie GITHUB_TOKEN-rechten: least privilege voor elke workflow.
Beperk secrets tot de stap die ze nodig heeft. Een secret in de env: van een stap staat alleen in de omgeving van die stap. Een secret dat op jobniveau is ingesteld, is zichtbaar voor elke stap, inclusief elke action van derden.
Zet productiesecrets achter environments. Met verplichte reviewers en regels voor deploymentbranches kan een workflow op een featurebranch, of een workflow die door een pull request is gestart, ze niet lezen.
Behandel pull_request_target en onbetrouwbare invoer als code-uitvoering. Vermijd pull_request_target tenzij u het nodig hebt, bouw er nooit pull-requestcode in, en geef eventgegevens door via omgevingsvariabelen. CodeQL kan workflows controleren op injectie en pwn-requestpatronen; het is sinds april 2025 algemeen beschikbaar voor GitHub Actions.
Schakel secret scanning en push protection in. Die vangen secrets die in de repository zijn gecommit, een ander lek dan de bovenstaande maar net zo gebruikelijk.
Beperk waarmee de job verbinding kan maken. De meeste jobs hebben een korte lijst bestemmingen nodig: pakketregisters, containerregisters en een paar API's. Kan de job alleen die bereiken, dan kan een secret dat door kwaadaardige code is gelezen nergens naartoe. Zie hoe een egressbeleid exfiltratie uit GitHub Actions stopt.
Roteer na elke blootstelling. Heeft een workflow een gecompromitteerde action of een gecompromitteerd pakket uitgevoerd, ga er dan van uit dat elk secret dat die kon lezen is gestolen. Trek ze allemaal in en geef ze opnieuw uit, niet alleen de secrets waarvan u denkt dat ze zijn gebruikt.
Waar CRACI in past
CRACI draait uw GitHub Actions-jobs op eigen runners: u wijzigt runs-on in craci, en de workflows, secrets en rungeschiedenis blijven in GitHub. Twee dingen die het doet, zijn gericht op de bovenstaande lekken.
- Een egressbeleid dat veilig dichtvalt. U bepaalt welke pakketbronnen, registers en hosts een job mag bereiken. Het beleid wordt gevalideerd voordat de job start en afgedwongen op de runner, dus een verbinding met iets anders wordt geblokkeerd, en u krijgt een waarschuwing als het beleid wordt overtreden.
- Een record van wat de job deed. CRACI legt elk pakket vast dat elke job daadwerkelijk heeft opgehaald, inclusief pakketten die uit caches zijn teruggezet, en een trace van de netwerkverbindingen van de job. Wanneer het volgende gecompromitteerde pakket wordt aangekondigd, ziet u welke builds het hebben binnengehaald en waarmee ze verbinding maakten, in plaats van te gokken op basis van lockfiles.
Elke job draait in een eigen virtuele machine, dus niets wat een job achterlaat, is er nog voor de volgende. CRACI scant uw code niet op secrets; houd daarvoor de secret scanning van GitHub.
Boek een demo om het record van een van uw eigen builds te zien.
