Most teams that bolt AI agents into CI eventually make the same bad shortcut. They give the workflow a long-lived cloud key, store it in GitHub Secrets, and hope review plus branch protection is enough.
That works right up until the wrong workflow runs, a forked change finds a path around the guardrails, or nobody remembers where else that key was reused.
For agentic systems, that trade is worse. A model can chain many actions quickly, so a leaked credential can do the same. GitHub Actions OIDC is the cleaner pattern: the workflow proves who it is at runtime, gets a short-lived cloud role, and the privilege disappears when the run ends.
Why this matters
If an AI workflow can open PRs, run evaluations, push artifacts, or trigger deployments, then credential shape matters as much as prompt shape. Static keys create silent blast radius, weak reviewability, and miserable rotation habits. OIDC moves the trust boundary into cloud IAM, where you can actually constrain repo, branch, workflow, and environment.
Architecture or workflow overview
flowchart LR
A[Pull request or deploy workflow] --> B[GitHub Actions runner]
B --> C[Request OIDC token]
C --> D[Cloud STS / workload identity]
D --> E[Short-lived role credentials]
E --> F[Agent job: eval, deploy, migrate]
F --> G[Logs, artifacts, approvals]The key design choice is that the agent never starts with a stored cloud secret. It earns a short-lived role from the cloud control plane after GitHub asserts the workflow identity.
Recommended trust shape
- Give only the credentialed job
id-token: write. - Match on repo plus workflow path, not repo alone.
- Split eval and deploy into separate roles.
- Put production behind environments, approvals, or both.
- Log caller identity before any write step.
Implementation details
Minimal GitHub Actions workflow
This setup is a good fit for AI-driven deploy or eval jobs because the credential only exists after GitHub attests the workflow identity.
name: ai-eval-and-deploy
on:
workflow_dispatch:
push:
branches: [master]
permissions:
contents: read
id-token: write
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-ai-eval
aws-region: us-east-1
- name: Run agent evaluation suite
run: |
python scripts/run_eval.py \
--dataset evals/release.json \
--output artifacts/eval-report.json
deploy:
needs: eval
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure deploy role via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-ai-deploy
aws-region: us-east-1
- name: Deploy bounded release
run: ./scripts/deploy.shTwo details are doing most of the work here. The deploy job is separated from eval, and production is attached to an environment so a human approval gate can exist outside the model loop.
AWS trust policy that actually constrains the workflow
A lot of OIDC tutorials prove login, but not safety. This is the part that makes the role meaningfully bounded.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:negiadventures/negiadventures.github.io:ref:refs/heads/master",
"repo:negiadventures/negiadventures.github.io:environment:production"
],
"token.actions.githubusercontent.com:job_workflow_ref": "negiadventures/negiadventures.github.io/.github/workflows/ai-eval-and-deploy.yml@refs/heads/master"
}
}
}
]
}If you skip workflow-path matching, any other workflow in the repo may inherit the same cloud power. That is the failure I see most often.
Optional session policy trim
Even with a decent role, trimming permissions per run keeps the agent inside a smaller box.
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/github-ai-deploy \
--role-session-name pr-ship-1842 \
--policy '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Action":["s3:PutObject","cloudfront:CreateInvalidation"],
"Resource":[
"arn:aws:s3:::my-site-bucket/*",
"arn:aws:cloudfront::123456789012:distribution/E123ABC456DEF"
]
}]
}'This matters when the same base role is used for multiple release paths, but a given run only needs one narrow write shape.
Terminal proof that the run is using ephemeral credentials
$ aws sts get-caller-identity
{
"UserId": "AROAXXXXXXXX:GitHubActions",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/github-ai-deploy/GitHubActions"
}
$ aws configure list
Name Value Type Location
---- ----- ---- --------
access_key ****************ABCD temporary-credentials-file
secret_key ****************wxyz temporary-credentials-file
region us-east-1 env AWS_REGIONIf the caller identity looks like a normal IAM user, you are not getting the benefit you think you are.
Tradeoffs and what went wrong
| Approach | Security posture | Operational pain | Best fit | What I would not do |
|---|---|---|---|---|
| Static cloud keys in GitHub Secrets | Weak, long-lived blast radius | Easy at first, painful later | Legacy pipelines you are retiring | New agent workflows |
| GitHub OIDC to cloud role | Strong, short-lived, auditable | Medium setup, low ongoing pain | Deploys, eval jobs, bounded writes | Wildcard trust policies |
| Self-hosted runner machine identity | Mixed, depends on host hardening | High ops burden | Internal-only specialized networks | Public repos or lightly managed hosts |
- Use separate roles for eval, deploy, and read-only inspection jobs.
- Bind trust to the workflow path as well as the repo.
- Prefer environment-gated production roles.
- Keep cloud IAM permissions narrower than the workflow permissions.
pull_request_targetcan quietly widen your threat model.- Reusable workflows deserve their own trust review.
- Self-hosted runners can turn a credential issue into a host issue.
- Artifact upload and production deploy should not share the same role by default.
Practical checklist
- No long-lived cloud keys stored in GitHub for this workflow
- Workflow requests
id-token: writeonly where needed - Trust policy checks repo, branch or environment, and workflow path
- Eval and deploy jobs use separate roles
- Production path requires environment approval or equivalent guardrail
- Logs prove the run used an assumed role session
- Session policies or narrow IAM policies limit write scope
Conclusion
If an AI agent can touch cloud systems, give it credentials that expire fast and only exist when the workflow proves who it is. GitHub Actions OIDC is one of the few changes that improves both security and day-two operations instead of making you choose.