Securing CI Pipelines with OIDC: No Long-Lived Secrets
Replace static AWS/GCP credentials in CI with short-lived tokens via OpenID Connect. Your pipeline gets temporary credentials that expire automatically — no more rotating secrets, no more leaked tokens.
Before you begin
- A GitHub Actions workflow that needs cloud access
- AWS or GCP account with IAM permissions
- Basic understanding of IAM roles and policies
The standard advice is "store your AWS_ACCESS_KEY_ID in GitHub Secrets." This works — until the key leaks, the rotation is overdue, or someone leaves the team. OIDC eliminates the credential entirely.
GitHub (and GitLab, CircleCI, and others) acts as an OIDC identity provider. AWS and GCP trust it. When a workflow runs, it requests a short-lived token from GitHub's OIDC endpoint and exchanges it for a cloud IAM role. The token expires in 15 minutes. There's nothing to rotate, nothing to leak.
How It Works
GitHub Actions job starts
→ GitHub issues a signed JWT (OIDC token) for this specific job
→ Workflow calls sts:AssumeRoleWithWebIdentity (AWS) or workloadIdentityPools (GCP)
→ Cloud validates the JWT signature against GitHub's OIDC discovery endpoint
→ Cloud returns temporary credentials scoped to the IAM role
→ Credentials expire when the job ends
The JWT contains claims that identify exactly where it came from:
sub:repo:your-org/your-repo:ref:refs/heads/mainrepository:your-org/your-repoenvironment:production(if using GitHub Environments)workflow:deploy.yml
You configure trust conditions to only grant the role if specific claims match — so only your repo's main branch can assume the production role.
Part 1: AWS Setup
Step 1: Create the OIDC Identity Provider in AWS
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1You only need to do this once per AWS account.
Verify it exists:
aws iam list-open-id-connect-providersStep 2: Create the IAM Role with a Trust Policy
The trust policy says: "Allow this role to be assumed if the OIDC token comes from GitHub and matches these claims."
1GITHUB_ORG="your-org"
2GITHUB_REPO="your-repo"
3AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
4
5cat > trust-policy.json <<EOF
6{
7 "Version": "2012-10-17",
8 "Statement": [
9 {
10 "Effect": "Allow",
11 "Principal": {
12 "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
13 },
14 "Action": "sts:AssumeRoleWithWebIdentity",
15 "Condition": {
16 "StringEquals": {
17 "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
18 "token.actions.githubusercontent.com:sub": "repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/main"
19 }
20 }
21 }
22 ]
23}
24EOF
25
26aws iam create-role \
27 --role-name GitHubActionsDeployRole \
28 --assume-role-policy-document file://trust-policy.jsonThe sub claim repo:org/repo:ref:refs/heads/main restricts assumption to merges to main only. To allow any branch:
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"To restrict to a specific GitHub Environment (requires GitHub Environments configured):
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"Step 3: Attach Permissions to the Role
Attach only what the pipeline needs:
1# For ECR push + EKS deploy
2aws iam attach-role-policy \
3 --role-name GitHubActionsDeployRole \
4 --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
5
6# Or create a minimal custom policy
7cat > deploy-policy.json <<EOF
8{
9 "Version": "2012-10-17",
10 "Statement": [
11 {
12 "Effect": "Allow",
13 "Action": [
14 "ecr:GetAuthorizationToken",
15 "ecr:BatchCheckLayerAvailability",
16 "ecr:PutImage",
17 "ecr:InitiateLayerUpload",
18 "ecr:UploadLayerPart",
19 "ecr:CompleteLayerUpload"
20 ],
21 "Resource": "*"
22 },
23 {
24 "Effect": "Allow",
25 "Action": [
26 "eks:DescribeCluster"
27 ],
28 "Resource": "arn:aws:eks:*:${AWS_ACCOUNT_ID}:cluster/my-cluster"
29 }
30 ]
31}
32EOF
33
34aws iam put-role-policy \
35 --role-name GitHubActionsDeployRole \
36 --policy-name DeployPolicy \
37 --policy-document file://deploy-policy.json
38
39ROLE_ARN=$(aws iam get-role \
40 --role-name GitHubActionsDeployRole \
41 --query "Role.Arn" --output text)
42echo "Role ARN: $ROLE_ARN"Step 4: Configure the GitHub Actions Workflow
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches: [main]
7
8permissions:
9 id-token: write # Required: lets the job request an OIDC token
10 contents: read
11
12jobs:
13 deploy:
14 runs-on: ubuntu-latest
15
16 steps:
17 - name: Checkout
18 uses: actions/checkout@v4
19
20 - name: Configure AWS credentials via OIDC
21 uses: aws-actions/configure-aws-credentials@v4
22 with:
23 role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
24 aws-region: ap-south-1
25
26 - name: Verify identity
27 run: aws sts get-caller-identity
28
29 - name: Log in to ECR
30 id: login-ecr
31 uses: aws-actions/amazon-ecr-login@v2
32
33 - name: Build and push image
34 env:
35 ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
36 IMAGE_TAG: ${{ github.sha }}
37 run: |
38 docker build -t $ECR_REGISTRY/my-app:$IMAGE_TAG .
39 docker push $ECR_REGISTRY/my-app:$IMAGE_TAGThe permissions: id-token: write at the top is required. Without it, the job can't request an OIDC token and the credential exchange fails.
Part 2: GCP Setup
Step 1: Create a Workload Identity Pool
1PROJECT_ID=$(gcloud config get-value project)
2POOL_NAME="github-actions-pool"
3
4gcloud iam workload-identity-pools create $POOL_NAME \
5 --project=$PROJECT_ID \
6 --location=global \
7 --display-name="GitHub Actions Pool"Step 2: Create a Provider in the Pool
1GITHUB_ORG="your-org"
2GITHUB_REPO="your-repo"
3
4gcloud iam workload-identity-pools providers create-oidc github-provider \
5 --project=$PROJECT_ID \
6 --location=global \
7 --workload-identity-pool=$POOL_NAME \
8 --display-name="GitHub Provider" \
9 --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
10 --attribute-condition="assertion.repository=='${GITHUB_ORG}/${GITHUB_REPO}'" \
11 --issuer-uri="https://token.actions.githubusercontent.com"Step 3: Bind the Provider to a Service Account
1SA_NAME="github-actions-sa"
2
3gcloud iam service-accounts create $SA_NAME \
4 --project=$PROJECT_ID \
5 --display-name="GitHub Actions Service Account"
6
7POOL_ID=$(gcloud iam workload-identity-pools describe $POOL_NAME \
8 --project=$PROJECT_ID \
9 --location=global \
10 --format="value(name)")
11
12gcloud iam service-accounts add-iam-policy-binding \
13 "${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
14 --project=$PROJECT_ID \
15 --role="roles/iam.workloadIdentityUser" \
16 --member="principalSet://iam.googleapis.com/${POOL_ID}/attribute.repository/${GITHUB_ORG}/${GITHUB_REPO}"
17
18# Grant the SA actual permissions
19gcloud projects add-iam-policy-binding $PROJECT_ID \
20 --member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
21 --role="roles/artifactregistry.writer"Step 4: Configure the GitHub Actions Workflow for GCP
1name: Deploy to GCP
2
3on:
4 push:
5 branches: [main]
6
7permissions:
8 id-token: write
9 contents: read
10
11jobs:
12 deploy:
13 runs-on: ubuntu-latest
14
15 steps:
16 - name: Checkout
17 uses: actions/checkout@v4
18
19 - name: Authenticate to GCP
20 uses: google-github-actions/auth@v2
21 with:
22 workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
23 service_account: github-actions-sa@your-project.iam.gserviceaccount.com
24
25 - name: Set up gcloud
26 uses: google-github-actions/setup-gcloud@v2
27
28 - name: Verify identity
29 run: gcloud auth listVerify No Secrets Are Stored
After this setup, check your repository's GitHub Secrets — there should be no AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or GCP service account JSON keys. The only configuration is the role ARN or workload identity provider path, which is not secret.
Run a workflow and check the logs — you'll see the OIDC token exchange happening transparently:
Assuming role arn:aws:iam::123456789012:role/GitHubActionsDeployRole
with OIDC token for repo:your-org/your-repo:ref:refs/heads/main
Assumed role successfully. Credentials expire in 3600s.
Debugging Common Errors
Not authorized to perform sts:AssumeRoleWithWebIdentity — The trust policy condition doesn't match the token's sub claim. Print the actual sub: add a step run: echo $ACTIONS_ID_TOKEN_REQUEST_URL and decode the token at jwt.io to see the exact sub value.
permissions block missing — The job can't request an OIDC token. Add permissions: id-token: write to the job or workflow level.
Token expired — The OIDC token is only valid for the duration of the workflow step. Don't cache it between workflow runs.
Official References
- GitHub Actions: OpenID Connect — How GitHub Actions OIDC tokens work and how to configure trust with cloud providers
- Configuring OIDC with AWS — Step-by-step guide for setting up GitHub → AWS OIDC federation without long-lived credentials
- Configuring OIDC with GCP — GitHub → GCP Workload Identity Federation setup
- AWS IAM: Creating OIDC Identity Providers — AWS IAM docs for creating OIDC providers and scoping trust policies
- Security Hardening for GitHub Actions — GitHub's comprehensive security guide covering OIDC, secrets, and least privilege
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.