GitHub Actions Beveiligen: Secrets, OIDC en Permissions Goed Regelen
Praktische tips om GitHub Actions workflows te beveiligen met OIDC, secret management, action pinning en minimale permissions.
Jean-Pierre Broeders
Freelance DevOps Engineer
GitHub Actions Beveiligen: Secrets, OIDC en Permissions Goed Regelen
De meeste teams hebben hun CI/CD pipeline draaiend binnen een middag. Werkt prima, tests draaien, deployment gaat automatisch. Maar vraag eens hoeveel van die pipelines écht beveiligd zijn. Het antwoord is meestal: niet zo best.
Een GitHub Actions workflow heeft toegang tot je code, je secrets, en vaak ook tot je cloud-omgeving. Dat is een aanvalsvector die regelmatig over het hoofd wordt gezien. Hier een paar dingen die het verschil maken tussen "het werkt" en "het werkt veilig".
Permissions: Standaard Te Ruim
GitHub geeft workflows standaard contents: read en actions: read permissions. Maar zodra er een GITHUB_TOKEN wordt gebruikt zonder expliciete scope, krijgt die token meer rechten dan nodig.
De fix is simpel maar wordt zelden toegepast: zet permissions expliciet op workflow-niveau.
permissions:
contents: read
packages: write
id-token: write
Door dit bovenaan je workflow te zetten, krijgt elke job precies de rechten die nodig zijn. Niet meer. Een workflow die alleen tests draait heeft geen packages: write nodig. Klinkt logisch, maar in de praktijk staat het er bijna nooit.
Nog beter: zet op repository-niveau de default permissions op read-only via Settings → Actions → General. Dan moet elke workflow expliciet aangeven wat het nodig heeft.
Secrets: Niet Alles in Repository Secrets Gooien
Repository secrets zijn handig, maar er zit een probleem: elke workflow in die repo kan ze gebruiken. Bij een groter team met meerdere workflows wordt dat snel onoverzichtelijk.
Environment secrets bieden meer controle. Door een environment aan te maken (bijv. production) en daar secrets aan te koppelen, beperkt de toegang zich tot workflows die dat specifieke environment gebruiken.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy naar productie
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
run: ./deploy.sh
Het voordeel: environments ondersteunen ook required reviewers. Niemand deployt naar productie zonder dat iemand op "approve" klikt. Dat klinkt als overhead, maar het voorkomt de paniek-deploy op vrijdagmiddag.
OIDC: Weg met die Langlevende Credentials
Dit is misschien wel de belangrijkste verbetering. Traditioneel worden cloud credentials (AWS access keys, Azure service principal secrets) als repository secrets opgeslagen. Die keys hebben geen vervaldatum, of eentje die zo ver in de toekomst ligt dat niemand ze roteert.
OpenID Connect (OIDC) lost dit op. In plaats van een statische key, vraagt de workflow een kortstondige token aan bij de cloud provider. Geen secrets meer die maanden in GitHub liggen.
Voor AWS ziet de setup er zo uit:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: eu-west-1
- name: Deploy
run: aws s3 sync ./dist s3://mijn-bucket
Geen AWS_ACCESS_KEY_ID meer in de secrets. De token die AWS uitgeeft is 15 minuten geldig. Zelfs als iemand die onderschept, is het window van misbruik minimaal.
Azure werkt vergelijkbaar met azure/login en een federated credential op de app registration. Google Cloud heeft google-github-actions/auth. Het patroon is overal hetzelfde.
Actions Pinnen op SHA
Een populaire best practice die bijna niemand doet: third-party actions vastzetten op een commit SHA in plaats van een versie-tag.
Het verschil:
# ❌ Riskant: tag kan overschreven worden
- uses: actions/checkout@v4
# ✅ Veilig: SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
Waarom? Een maintainer (of aanvaller met toegang) kan een tag verplaatsen naar een andere commit. Bij een SHA is dat onmogelijk. De Dependabot van GitHub kan deze SHA's automatisch updaten via PR's, dus het onderhoud valt mee.
Voor interne actions binnen dezelfde organisatie is dit minder kritisch. Maar voor alles van derden: pin op SHA.
| Methode | Veiligheid | Onderhoud |
|---|---|---|
| Tag (v4) | Laag — tag kan verschuiven | Makkelijk |
| Branch (main) | Zeer laag — verandert continu | Geen |
| SHA | Hoog — immutable | Dependabot helpt |
Fork PR's: Een Onderschat Risico
Open source projecten krijgen pull requests van forks. Standaard draaien workflows op fork PR's met beperkte rechten (geen toegang tot secrets). Maar als pull_request_target wordt gebruikt in plaats van pull_request, verandert dat. Dan draait de workflow in de context van de base repo, mét toegang tot secrets.
Dit is bewust zo ontworpen voor specifieke use cases, maar het wordt regelmatig verkeerd ingezet. De vuistregel: gebruik pull_request_target alleen als je weet wat je doet, en checkout dan nooit de PR-code met actions/checkout zonder de ref expliciet te beperken.
Audit Logging
GitHub Enterprise biedt audit logs voor Actions. Maar ook zonder Enterprise valt er iets te doen. De actions/toolkit heeft een core.setSecret() functie die voorkomt dat waarden in de logs verschijnen. Gebruik dat voor alles wat gevoelig is, ook als het niet uit secrets komt.
- name: Mask dynamic secret
run: echo "::add-mask::${DYNAMIC_VALUE}"
Kleine moeite, maar het voorkomt dat tokens of API responses zichtbaar worden in build logs die mogelijk openbaar zijn.
Samengevat
Pipeline security is geen eenmalige actie. Het is een hygiëne-ding, net als tests schrijven of dependencies updaten. De kern:
- Permissions op workflow-niveau, default read-only op repo-niveau
- Environment secrets in plaats van repository secrets voor gevoelige omgevingen
- OIDC voor cloud access, weg met statische keys
- SHA pinning voor third-party actions
- Fork PR workflows goed configureren
Geen van deze stappen kost meer dan een uurtje om in te richten. Het verschil tussen een pipeline die "gewoon werkt" en eentje die veilig werkt, is verrassend klein.
