Security
Audit-checklist voor coding agents: voor toegang tot main
Een autonome coding agent herschreef in één nachtelijke run 184 package-manifests in een Fedora CI-fleet. Dit is de audit die wij nu draaien voor een agent schrijfrechten op main krijgt.

Om 03:14 Bangkok-tijd opende een operations lead Slack en zag één bericht in het on-call kanaal: "184 PRs van de agent, allemaal gemerged, allemaal groen." Zijn CI-fleet draaide op Fedora, de agent had een GitHub App-token met schrijfrechten, en de workflow die Dependabot-achtige PRs automatisch merget maakte geen onderscheid tussen Dependabot en "de nieuwe agent waar we mee piloten." Package-manifests waren herschreven in 41 services. De helft van de herschrijvingen klopte. De andere helft pinde een dev-build van een transitive die niet bestond op de publieke mirror. De standup begon met een rollback-plan en een excuus.
Wij zagen dit vorige maand gebeuren bij een klant. Niemand werd ontslagen. Er lekte niks. Maar de productie-deploypipeline lag negen uur stil terwijl twee engineers elke commit terugdraaiden. De agent had niets kwaadaardigs gedaan. Hij deed precies wat we hem gevraagd hadden, op de snelheid die we hem toestonden.
Deze post is de audit die wij nu draaien voor een autonome coding agent pull requests mag openen tegen main op de stack van een klant onder de €30M. We gaan uit van GitHub Actions en Kubernetes, want dat zien we het vaakst. De lijst is bewust kort. Elk item heeft een echt probleem afgevangen.
Wat de 184 manifests ons leerden
De les was niet "agents zijn gevaarlijk." De les was dat de guardrails voor menselijke contributors en die voor agents van buitenaf identiek lijken, maar in compleet andere vormen falen. Een mens die een niet-bestaande transitive pint, ziet het bij de eerste dnf install-fout en stopt. Een agent opent een nieuwe PR om de fout op te lossen, en nog een, en nog een, want dat is het reward-signaal dat hij meekreeg.
De oplossing is niet de agent beter oordeelsvermogen geven. De oplossing is zorgen dat de blast radius van een fout oordeel klein genoeg is om voor het ontbijt terug te draaien. Hetzelfde patroon speelt branchebreed op grotere schaal dan de onze, op zichtbaardere stacks. De discussie aan de vendor-kant over hoeveel autonomie een agent default mee moet krijgen, is dezelfde discussie vanaf de andere kant van de tafel.
Branch protection is noodzakelijk, niet genoeg
De eerste vraag van elk team is: "Lost branch protection dit niet op?" Nee. Het verandert wie er mag mergen, niet wie er mag schrijven. Een agent met PR-open-rechten plus een medeplichtige workflow die "trusted" authors automatisch goedkeurt, omzeilt elke regel die alleen het reviewer-veld controleert. We zagen er drie varianten van in het wild: GitHub Actions die PRs op basis van een label auto-approven, bots die gh pr review --approve draaien namens een service account, en de klassieke "required reviewers"-lijst waar de GitHub App van de agent zelf in stond.
De echte check is: kan de identiteit van de agent, direct of transitief, voldoen aan elke verplichte protection-regel op main? Als dat zo is, is branch protection decoratie. De audit kost tien minuten per repo en vindt bijna altijd één pad dat niemand had dichtgezet.
Als je required-reviewers-lijst een GitHub App of service account bevat die de agent zelf kan aanroepen, kan de agent zijn eigen PRs goedkeuren. Audit het lidmaatschap van elk team en elke CODEOWNERS-regel die merge-rechten geeft.
GitHub Actions: token-scope, OIDC en workflow_run
De default GITHUB_TOKEN in een workflow heeft meer macht dan de meeste teams beseffen. Lees eens per kwartaal de automatic token authentication docs. Schrijf daarna op papier op wat de workflow van je agent met dat token kan doen. Het antwoord is meestal "meer dan het team dacht."
Drie controles wegen het zwaarst:
- Permissions-block op workflow-root. Default naar
permissions: {}en voeg alleen de scopes toe die een job daadwerkelijk nodig heeft.contents: readvoor de checkout,pull-requests: writevoor de comment, verder niks. - OIDC in plaats van langlevende secrets. Als de agent naar een cloud deployt, federate via OIDC en scope de trust policy op één repo, één branch en één workflow-bestand. Een gelekte
AWS_ACCESS_KEY_IDin repo-secrets is een jaar aansprakelijkheid; een OIDC role-assumption is een sessie van vijftien minuten. - workflow_run is een footgun. Een workflow getriggerd door
workflow_rundraait met de permissions van het workflow-bestand op de default branch, niet van de PR. Dat is de juiste default voor security, en de verkeerde default als je agent het workflow-bestand bewerkt en verwacht dat de nieuwe versie meteen draait. Lees de spec goed voor je iets anders aansluit.
Een workflow waarin de agent PRs mag openen, maar niet direct mag pushen naar main, geen repo-secrets mag lezen en geen downstream workflows mag triggeren:
name: agent-propose
on:
workflow_dispatch:
inputs:
task:
type: string
required: true
permissions: {}
jobs:
propose:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: run agent
env:
AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }}
run: ./scripts/run-agent.sh "${{ inputs.task }}"
- name: open pr
uses: peter-evans/create-pull-request@v6
with:
branch: agent/${{ github.run_id }}
base: main
title: "agent: ${{ inputs.task }}"
body: "Proposed by agent run ${{ github.run_id }}."
labels: agent-proposed
Kubernetes: namespace, RBAC en de kill switch
Als de agent in je cluster draait (gesandboxte test-runs, ephemerale preview-envs, wat dan ook), krijgt hij zijn eigen namespace, zijn eigen ServiceAccount en een RBAC-rol die precies opsomt wat hij mag aanraken. De fout die we het vaakst zien: edit toekennen op namespace-niveau "alleen even voor de pilot" en het nooit meer terugdraaien. Drie maanden later heeft de agent in alles behalve naam cluster-admin, en niemand weet meer waarom.
Het minimum dat wij uitleveren ziet er zo uit:
apiVersion: v1
kind: Namespace
metadata:
name: agent-sandbox
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent
namespace: agent-sandbox
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: agent-sandbox
name: agent-runner
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: agent-sandbox
name: agent-runner
subjects:
- kind: ServiceAccount
name: agent
namespace: agent-sandbox
roleRef:
kind: Role
name: agent-runner
apiGroup: rbac.authorization.k8s.io
Twee dingen die niet vanzelfsprekend zijn. Ten eerste: de restricted Pod Security Standard als label op de namespace is niet onderhandelbaar. Het blokkeert privileged containers, host mounts en een dozijn andere ontsnappingen die een agent per ongeluk via zijn tooling-laag kan opvragen. Ten tweede: de kill switch zit één kubectl-commando verderop: kubectl scale deployment agent --replicas=0 -n agent-sandbox. Iedereen in de rotatie moet dat zonder opzoeken weten. Oefen het tijdens de onboarding, zoals een brandoefening.
De audit-checklist
Dit is de echte lijst die we met een klant doorlopen voor we een agent rechten geven om PRs te openen tegen main. Hij is genummerd, want we vinken elk item af in een gedeeld document en weigeren de knop om te zetten tot elke regel groen is.
- Identiteit. De agent heeft een eigen GitHub App (geen PAT, geen user-account). De installatie van die App is gescoped op specifieke repos, niet op de hele org.
- Token-scope. Elke workflow die de agent kan aanroepen heeft een expliciet
permissions:-block op root-niveau. Geen enkele workflow gebruikt de default token-scope. - Branch protection.
mainvereist PR-review van een mens die in CODEOWNERS staat. De identiteit van de agent staat, direct noch transitief, in een CODEOWNERS-regel diemainraakt. - Auto-merge. Geen workflow merget automatisch PRs met een label van de agent. Geen workflow keurt automatisch PRs goed van de identiteit van de agent.
- Secrets. De workflows van de agent lezen geen repo- of org-secrets direct. Cloud-toegang loopt via OIDC met een trust policy die gescoped is op het pad van het workflow-bestand.
- Rate limit. Een concurrency-group op repo-niveau beperkt de agent tot één open PR tegelijk. Wil je parallellisme, zet de limiet expliciet en documenteer waarom.
- Blast radius. De agent kan geen bestanden wijzigen onder
.github/workflows/,deploy/of welk pad dan ook dat invloed heeft op hoe zijn eigen code draait. CODEOWNERS dwingt dat af. - Kubernetes. Draait de agent in-cluster, dan heeft hij een eigen namespace met
pod-security.kubernetes.io/enforce: restricted, een eigen ServiceAccount met token auto-mount uit, en een RBAC-Role die precies de verbs en resources opsomt die hij nodig heeft. - Kill switch. Elke on-call engineer kan de agent stoppen met één commando, in platte tekst op de runbook. Het commando vereist geen credentials opzoeken.
- Audit log. Elke PR die de agent opent linkt terug naar de prompt, de model-versie en de workflow-run die hem opleverde. Kun je in drie klikken niet reconstrueren "waarom deed de agent dit", dan kun je een runaway niet debuggen.
- Data retention. Je weet wat je modelvendor opslaat en hoe lang. Retention-windows op enterprise agent-tiers, vaak default dertig dagen, moeten in je DPA staan voor je live gaat.
- Dry run. De eerste week dat de agent live is, opent hij PRs tegen een fork of een langlopende
agent-pilot-branch, niet tegenmain. Je merget met de hand. Je leest elke diff.
Wat we eerder hadden willen checken
Bij de klant waar de 184 manifests landden hadden we vooraf liever item 7 grondiger gecheckt. De agent had geen schrijfrechten op main. Wel had hij schrijfrechten op renovate.json, dat hij bewerkte om een nieuw package-ecosysteem toe te voegen, wat de auto-merge workflow triggerde op een label waarvan het team vergeten was dat het bestond. Het pad naar main was drie sprongen. De audit vond het binnen een kwartier. De fix kostte vijf minuten.
Een tweede bijna-incident op hetzelfde fleet kwam uit een workflow-bestand dat de agent herschreef omdat de linter erom vroeg. De diff klopte, was klein, en zou elke toekomstige cloud-deploy via een andere OIDC-rol hebben geleid. Niemand aan de review-kant zag het. De blast-radius-regel (nogmaals item 7) ving het bij de volgende push, omdat .github/workflows/ al verboden terrein was voor de identiteit van de agent. Eén regel, twee incidenten, allebei stil.
Toen we de autonome PR-pipeline opzetten voor een logistieke klant met een Fedora-gebaseerde CI-fleet, was het probleem datzelfde auto-merge label, weggestopt in een workflow die niemand sinds 2022 had aangeraakt. We hebben het opgelost met een process automation-stap die bij elke push elk workflow-bestand in de repo lint tegen de checklist hierboven, en de build laat falen zodra een item afwijkt. Saai, mechanisch, en heeft sindsdien twee bijna-incidenten afgevangen.
Eén ding dat je vandaag kunt doen: open de settings van je repo, klik door naar Actions, dan General, dan Workflow permissions, en kijk of de default "Read and write" is. Zo ja, zet hem op read-only en laat elke workflow in zijn eigen permissions:-block meer aanvragen. Die ene wijziging verkleint de blast radius van elke workflow die je hebt, inclusief de workflows die je nog moet schrijven.
Kern
Behandel een autonome coding agent als een externe op dag één: eigen identiteit, scoped credentials, geen auto-merge en een kill switch die elke operator uit zijn hoofd kent.
FAQ
Houdt branch protection een agent tegen op main?
Alleen als de identiteit van de agent geen enkele verplichte protection-regel kan vervullen. Audit CODEOWNERS en reviewer-teams op service-accounts die de agent kan aanroepen. Anders is het decoratie.
Moet een agent een Personal Access Token of een GitHub App gebruiken?
Altijd een GitHub App. Scope de installatie op specifieke repos, niet op de org. PATs hangen aan een menselijk account en erven te veel org-brede reikwijdte voor een autonome actor.
Wat is de grootste GitHub Actions-valkuil?
De default GITHUB_TOKEN staat in veel repos nog op read en write. Zet hem op org-niveau op read-only en laat workflows in hun permissions-block meer aanvragen.
Hoe snel kunnen we een runaway agent stoppen?
Zo snel als de traagste on-call engineer één kubectl scale of gh workflow disable kan draaien. Zet het commando in platte tekst op de runbook. Test het tijdens de onboarding.