GitHub Actions — Keyless AWS Deploy via OIDC
Stop storing long-lived AWS access keys in GitHub. Use OIDC to assume an IAM role from your workflow — the modern, secure way.
OIDC means every workflow run gets a short-lived federated token instead of a static AWS key. If your repo is compromised, attackers get nothing useful. This template is the GH Actions + IAM trust policy + sample deploy job — everything you need.
1. Create the OIDC provider in AWS (one-time)
You only need ONE per AWS account, regardless of how many repos use it.
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}2. IAM role that GitHub can assume
The trust policy locks who can assume this role: only workflows from one repo, optionally only from main branch / specific environment.
data "aws_caller_identity" "current" {}
variable "repo" { default = "gangadharure/cloudadhar" } # OWNER/REPO
resource "aws_iam_role" "gha_deploy" {
name = "github-actions-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Lock to one repo. Tighten further with refs/heads/main or environment:prod
"token.actions.githubusercontent.com:sub" = "repo:${var.repo}:*"
}
}
}]
})
}
# Attach managed/inline policies as needed:
resource "aws_iam_role_policy_attachment" "ecr" {
role = aws_iam_role.gha_deploy.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}
output "gha_role_arn" { value = aws_iam_role.gha_deploy.arn }3. The GitHub Actions workflow
Set `id-token: write` permissions, then use the official aws-actions/configure-aws-credentials action.
name: Build & Deploy
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC
contents: read
env:
AWS_REGION: ap-south-1
ECR_REPO: cloudadhar/api
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
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-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Login to ECR
id: ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build & push image
run: |
IMAGE=${{ steps.ecr.outputs.registry }}/${{ env.ECR_REPO }}:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
- name: Trivy scan
uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: ${{ env.IMAGE }}
severity: CRITICAL,HIGH
exit-code: '1'
- name: Deploy via Helm
run: |
aws eks update-kubeconfig --name prod-eks
helm upgrade --install api ./chart \
-n app -f chart/values-prod.yaml \
--set image.tag=${{ github.sha }} \
--wait --timeout 5m4. Verify
Run the workflow. You should see `Successfully assumed role` in the AWS step — no keys involved. Check CloudTrail: events are tagged with the GitHub repo + ref.
Tighten further (production)
Replace `:*` with `:ref:refs/heads/main` to only allow main branch, or `:environment:production` if you use GitHub Environments with required reviewers. Add a `branch-protection` rule requiring approval for main.
"token.actions.githubusercontent.com:sub" = [
"repo:gangadharure/cloudadhar:ref:refs/heads/main",
"repo:gangadharure/cloudadhar:environment:production"
]Want me to implement this in your environment?
Cloudadhar offers paid setup engagements: I bring the template above into your AWS account, wire it up to your CI/CD, and walk your team through it.