Hardening CI/CD Runners with Least Privilege Cloud IAM and OIDC

Open your GitHub repository secrets dashboard right now. Do you see AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY?
If so, your delivery pipeline is running on a time bomb. Static, long-lived API keys stored in CI systems are prime targets for attackers. If a third-party action in your pipeline is compromised or an untrusted pull request logs environment variables, an attacker gains permanent, unmonitored access to your cloud account.
Modern cloud engineering teams eliminate static cloud credentials completely using OpenID Connect (OIDC) and short-lived, least-privilege IAM roles.
Static Secrets vs. OIDC Federation
| Capability | Static IAM Access Keys | OIDC Federated IAM Roles |
|---|---|---|
| Credential Lifespan | Months or years (until manually rotated) | 15 to 60 minutes (automatic expiry) |
| Rotation Overhead | High (requires updating CI dashboards) | Zero (generated dynamically per build) |
| Risk of Leakage | Permanent compromise of cloud tenant | Useless within minutes of job completion |
| Scope Granularity | Bound to static user identity | Scoped strictly to repository, branch, or tag |
| Auditability | Difficult to associate actions with exact commit | Every API call logs the exact workflow run ID |
How OIDC Federation Works
Instead of storing a secret key, your CI runner requests a signed cryptographic JSON Web Token (JWT) from GitHub, presents it to your cloud provider (AWS, GCP, or Azure), and receives temporary credentials:
[GitHub Runner] --> Requests signed OIDC token from GitHub
[GitHub OIDC] --> Issues cryptographically signed JWT with repo & branch claims
[AWS / GCP IAM] --> Verifies signature against GitHub's public keys
[Cloud IAM Role] --> Assumes role and returns 1-hour temporary STS credentials
Configuring AWS IAM Role for GitHub Actions OIDC
Configure an IAM Trust Policy that restricts role assumption strictly to your production repository and main branch:
{
"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:my-organization/my-app:ref:refs/heads/main"
}
}
}
]
}
Notice the StringLike condition: if an attacker forks your repository or opens a malicious pull request from a feature branch, the cloud provider rejects the token because the branch claim does not match refs/heads/main.
Consuming OIDC in Your Workflow
In your GitHub Actions workflow, request the id-token: write permission and authenticate without a single stored secret:
# .github/workflows/deploy.yml
name: Deploy Production
on:
push:
branches: [main]
permissions:
id-token: write # Required for requesting the OIDC JWT
contents: read
jobs:
deploy:
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-actions-prod-deployer
aws-region: ap-south-1
- name: Deploy Infrastructure
run: |
echo "Authenticated using temporary STS token!"
aws s3 sync ./dist s3://company-production-assets/
To learn how to modernize and protect your cloud delivery infrastructure, review our managed cloud services and discover why cloud account ownership and access boundaries matter after launch.


