Hardcoded cloud keys in CI pipelines are still one of the fastest ways to turn a small leak into a major breach. In 2026, the safer baseline is short-lived credentials issued just-in-time through OpenID Connect (OIDC). In this guide, you will build a production-ready GitHub Actions to AWS deployment flow with zero long-lived secrets, branch-level trust boundaries, and auditable least-privilege IAM policies.
Why OIDC is the default in 2026
Traditional CI setups store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in repository or org secrets. Even when encrypted at rest, these are static credentials with broad blast radius. OIDC replaces them with signed identity tokens from GitHub that AWS validates at assume-role time.
- No long-lived AWS keys in GitHub secrets.
- Credentials are short-lived and scoped to one workflow run.
- IAM trust can be tied to repo, branch, environment, and workflow context.
- CloudTrail captures role assumption events for cleaner audits.
Architecture overview
The flow is simple:
- GitHub Actions job requests an OIDC token.
- AWS IAM role trust policy validates token claims (
aud,sub). - AWS STS returns temporary credentials.
- Workflow deploys using those credentials and expires automatically.
Step 1: Create the GitHub OIDC provider in AWS
Run this once per AWS account (if not already configured):
aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1Many accounts already have this provider. If the command says it exists, reuse it.
Step 2: Create a tightly scoped IAM role trust policy
Here is a trust policy that allows only one repository and one branch to assume the role:
{
"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",
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}Important claim patterns
repo:org/repo:ref:refs/heads/mainfor branch-based deployments.repo:org/repo:environment:productionfor GitHub Environment-based controls.- Use
StringLikeonly when you truly need wildcards.
Step 3: Attach least-privilege permissions
Create a role policy for only what deployment needs. Example for shipping a container to ECR and updating ECS service:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecs:DescribeServices",
"ecs:UpdateService"
],
"Resource": [
"arn:aws:ecs:ap-south-1:123456789012:service/prod-cluster/api-service"
]
}
]
}Avoid giving this role wildcard admin privileges. Keep one role per deployment target to reduce lateral movement risk.
Step 4: Configure GitHub Actions workflow
Add this workflow file at .github/workflows/deploy.yml:
name: Deploy API
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials from OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-prod-deploy
aws-region: ap-south-1
role-session-name: gha-prod-${{ github.run_id }}
- name: Login to ECR
run: |
aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-south-1.amazonaws.com
- name: Build and push image
run: |
IMAGE=123456789012.dkr.ecr.ap-south-1.amazonaws.com/my-api:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
- name: Deploy service
run: |
aws ecs update-service --cluster prod-cluster --service api-service --force-new-deploymentStep 5: Add production safety rails
OIDC removes key management pain, but safe delivery still needs guardrails:
- Use GitHub Environments with required reviewers for production.
- Pin third-party actions by commit SHA, not floating tags.
- Protect
mainwith required checks and signed commits if possible. - Set session duration low (for example, 15 to 60 minutes).
- Enable CloudTrail alerts for unusual
AssumeRoleWithWebIdentityevents.
Troubleshooting common OIDC failures
1) “Not authorized to perform sts:AssumeRoleWithWebIdentity”
Your trust policy claim matching is likely wrong. Verify repo owner, repo name, and exact branch ref in the sub condition.
2) Workflow has no OIDC token
Ensure workflow permissions include id-token: write. Without that, token minting is blocked.
3) Wrong audience claim
For AWS, the expected audience is sts.amazonaws.com. Mismatch causes silent trust failures.
Hardening pattern for multi-account AWS
For serious environments, use one deployment account role that can assume narrowly scoped target roles in each environment account. This lets you centralize GitHub trust while preserving account isolation.
GitHub OIDC Role (Deploy Account) -> sts:AssumeRole -> Env Account Deploy RoleKeep explicit external IDs and resource tags where possible, and separate dev/staging/prod roles.
Final checklist
- OIDC provider exists in AWS account.
- Trust policy matches exact repository and branch/environment.
- Role policy is least privilege for deployment actions only.
- Workflow sets
permissions.id-token: write. - Production uses environment approval and branch protections.
Once this is in place, your pipeline is safer, cleaner, and easier to audit. In 2026, passwordless CI/CD is not an advanced option, it is the operational baseline every serious engineering team should adopt.

Leave a Reply