OIDC for CI/CD
OIDC (OpenID Connect) for CI/CD allows pipelines to assume cloud provider roles using short-lived tokens instead of long-lived static credentials. GitHub Actions, GitLab CI, and other platforms issue OIDC tokens that AWS, GCP, and Azure accept in exchange for temporary access credentials.
Understanding OIDC for CI/CD
What Is OIDC for CI/CD in Simple Terms
OIDC for CI/CD eliminates the most dangerous secret in your pipeline: long-lived cloud credentials. Instead of storing an AWS access key and secret key as CI secrets (where they never expire, must be manually rotated, and can be exfiltrated), OIDC lets GitHub Actions prove its identity to AWS using a cryptographically signed short-lived token. AWS exchanges this token for temporary 15-minute credentials scoped to exactly the permissions you specify.
No secrets to store. No rotation schedules. No leaked credentials to revoke. If the token is somehow intercepted, it expires in minutes.
How It Works
+------------------------------------------+| GitHub Actions job starts |+------------------------------------------+ | v+------------------------------------------+| GitHub generates OIDC token || Token contains: || iss: token.actions.githubusercontent.com|| sub: repo:razorpay/payment-api:ref:main|| aud: sts.amazonaws.com || exp: now + 15 minutes |+------------------------------------------+ | sent to AWS STS | v+------------------------------------------+| AWS STS validates token || Checks: issuer, subject, audience || Matches IAM role trust policy |+------------------------------------------+ | v+------------------------------------------+| AWS returns temporary credentials || Access key, secret key, session token || Valid for 15 minutes || Scoped to the assumed IAM role |+------------------------------------------+ | v+------------------------------------------+| Job uses credentials for AWS operations || Push to ECR, deploy to EKS, etc. |+------------------------------------------+GitHub Actions OIDC with AWS setup:
## Step 1: Workflow permissions (required for OIDC)permissions: id-token: write ## allows requesting OIDC token contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 ## Step 2: Use the OIDC action -- no secrets needed - name: Configure AWS credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/github-deploy-role aws-region: ap-south-1 ## role-session-name appears in CloudTrail logs role-session-name: github-actions-${{ github.run_id }} ## Now AWS CLI and SDKs work without explicit credentials - name: Push to ECR run: | aws ecr get-login-password --region ap-south-1 | \\ docker login --username AWS --password-stdin \\ 123456789.dkr.ecr.ap-south-1.amazonaws.com docker push 123456789.dkr.ecr.ap-south-1.amazonaws.com/payment-api:${{ github.sha }}AWS IAM trust policy for GitHub OIDC:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789: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:razorpay/payment-api:ref:refs/heads/main" } } } ]}GitLab CI OIDC with AWS:
deploy: image: amazon/aws-cli:latest id_tokens: AWS_OIDC_TOKEN: aud: sts.amazonaws.com script: - | CREDS=$(aws sts assume-role-with-web-identity \\ --role-arn arn:aws:iam::123456789:role/gitlab-deploy-role \\ --role-session-name gitlab-ci-$CI_JOB_ID \\ --web-identity-token $AWS_OIDC_TOKEN \\ --query 'Credentials' \\ --output json) export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .AccessKeyId) export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .SecretAccessKey) export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .SessionToken) aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URLPractical Commands
## Create OIDC provider in AWS (one-time setup)aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com \ --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 ## Create IAM role with trust policyaws iam create-role \ --role-name github-deploy-role \ --assume-role-policy-document file://trust-policy.json ## Attach permissions to the roleaws iam attach-role-policy \ --role-name github-deploy-role \ --policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy ## Test OIDC locally (debug)aws sts get-caller-identity ## shows which role is assumedTroubleshooting
| Symptom | Check | What to Look For |
|---|---|---|
| OIDC auth fails | Trust policy sub condition | Repo name, branch, ref format must match exactly |
| Credentials expire during job | Role session duration | Increase max_session_duration on IAM role |
| Wrong role assumed | role-to-assume ARN | Check ARN for typos, account ID |
| Works on main, fails on branch | Trust policy condition | StringLike with * for all branches |
TipUse
StringLikewith a wildcard in the trust policy subject condition to allow all branches:"repo:razorpay/payment-api:*". For production deployments, restrict to main only:"repo:razorpay/payment-api:ref:refs/heads/main". This lets feature branches use OIDC for staging but prevents them from accessing production credentials.
RememberThe
id-token: writepermission must be declared in the GitHub Actions workflow for OIDC to work. Without it, the workflow cannot request an OIDC token from GitHub and theconfigure-aws-credentialsaction will fail silently or with a cryptic error. Always declare this permission explicitly.
SecurityScope IAM roles created for GitHub Actions to the minimum permissions needed. A role used for ECR push should have
ecr:GetAuthorizationTokenandecr:BatchCheckLayerAvailability,ecr:PutImage-- notAdministratorAccess. The trust policy restricts WHICH pipelines can assume the role; the IAM policy restricts WHAT they can do after assuming it. Both must be correctly scoped.
Common MistakeForgetting to add the GitHub OIDC provider to your AWS account before creating the IAM role. The trust policy references the OIDC provider ARN -- if the provider does not exist in your account, the
AssumeRoleWithWebIdentitycall will fail with an error about an invalid principal. Create the OIDC provider first, then create the IAM role.
Frequently Asked Questions
What specific security problem does OIDC in CI/CD eliminate compared to storing cloud credentials as pipeline secrets?
Long-lived access keys stored as CI secrets are a standing liability — if a pipeline config leaks, a log line accidentally prints a value, or a former employee retains repo access, those keys work indefinitely until manually rotated. OIDC has the CI platform (GitHub Actions, GitLab CI) issue a short-lived, cryptographically signed identity token for each job, which the cloud provider exchanges for temporary credentials — typically valid for under an hour — scoped to a pre-configured trust relationship, with nothing long-lived to leak.
What's a common misconfiguration when setting up OIDC trust between CI and a cloud provider?
Configuring the trust policy's `sub` (subject) claim too broadly — like trusting any workflow from an entire GitHub org instead of a specific repo and branch — effectively lets any repo in that org assume the role, defeating the purpose of scoping. The claim condition should be as narrow as the use case allows, commonly restricted to `repo:org/repo:ref:refs/heads/main`, so only the intended branch of the intended repository can obtain credentials.