CI/CD

How to Set Up CI/CD for AWS with GitHub Actions — No Access Keys Needed

By Akshay Ghalme·March 30, 2026·9 min read

To set up CI/CD for AWS with GitHub Actions without access keys, use OIDC (OpenID Connect) authentication. Register GitHub as an OIDC identity provider in IAM, create a role with a trust policy scoped to your repo and branch, and use the aws-actions/configure-aws-credentials action with role-to-assume. No long-lived credentials stored anywhere — temporary tokens are issued per workflow run and expire automatically.

If you are deploying to AWS from GitHub Actions, there is a good chance you have an IAM access key sitting in your GitHub secrets right now. It works. It is also a security risk that most teams accept because they do not know there is a better way.

The problem with access keys: they are long-lived credentials. They do not expire unless you rotate them manually. If they leak through a log, a fork, or a compromised dependency, someone has access to your AWS account until you notice and revoke them.

OIDC fixes this entirely. GitHub proves its identity to AWS, AWS issues temporary credentials that last only for the duration of the workflow run, and there are zero secrets stored anywhere.

What You Will Build

  • OIDC identity provider — registers GitHub as a trusted identity in your AWS account
  • IAM role — GitHub Actions assumes this role to get temporary AWS credentials
  • ECR repository — stores your Docker images with scanning enabled
  • GitHub Actions workflow — builds, scans, pushes, and deploys with zero stored credentials

How OIDC Works (Simple Version)

The flow is straightforward:

  1. Your GitHub Actions workflow starts and requests an OIDC token from GitHub
  2. The workflow sends this token to AWS STS (Security Token Service)
  3. AWS verifies the token against the OIDC provider you registered
  4. If the token is valid AND the repo/branch matches your trust policy, AWS issues temporary credentials
  5. The workflow uses these credentials to push to ECR, deploy to ECS, etc.
  6. When the job ends, the credentials expire automatically

No keys stored anywhere. No secrets to rotate. No risk of credential leaks.

Prerequisites

  • An AWS account
  • A GitHub repository
  • Terraform 1.5 or later
  • A Docker application you want to deploy

Step 1: Create the OIDC Identity Provider in AWS

First, tell AWS to trust GitHub as an identity provider:

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = ["sts.amazonaws.com"]

  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]

  tags = {
    Name = "github-actions-oidc"
  }
}

The client_id_list with "sts.amazonaws.com" is the audience value that GitHub includes in its OIDC tokens. The thumbprint is GitHub's certificate fingerprint — AWS uses it to verify the token really came from GitHub.

You only need to create this OIDC provider once per AWS account. Even if you have 50 repos, they all share the same provider. Each repo gets its own IAM role with a specific trust policy.

Step 2: Create the IAM Role for GitHub Actions

This is where you control which repos and branches can assume the role:

resource "aws_iam_role" "github_actions" {
  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 = {
            "token.actions.githubusercontent.com:sub" = "repo:YOUR_GITHUB_USERNAME/YOUR_REPO:ref:refs/heads/main"
          }
        }
      }
    ]
  })
}

# Attach permissions for ECR and ECS
resource "aws_iam_role_policy" "deploy_permissions" {
  name = "deploy-permissions"
  role = aws_iam_role.github_actions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload",
          "ecr:DescribeImageScanFindings"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "ecs:UpdateService",
          "ecs:DescribeServices",
          "ecs:RegisterTaskDefinition",
          "ecs:DescribeTaskDefinition"
        ]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = "iam:PassRole"
        Resource = "*"
        Condition = {
          StringEquals = {
            "iam:PassedToService" = "ecs-tasks.amazonaws.com"
          }
        }
      }
    ]
  })
}

The sub condition is critical. It restricts the role to a specific repo AND a specific branch. Change YOUR_GITHUB_USERNAME/YOUR_REPO to your actual values. The :ref:refs/heads/main part means only pushes to the main branch can trigger deployments.

Step 3: Create the ECR Repository

resource "aws_ecr_repository" "app" {
  name                 = var.app_name
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }

  tags = {
    Name = var.app_name
  }
}

# Clean up old images — keep only the last 10
resource "aws_ecr_lifecycle_policy" "app" {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Keep only last 10 images"
        selection = {
          tagStatus   = "any"
          countType   = "imageCountMoreThan"
          countNumber = 10
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}

Image scanning on push catches known vulnerabilities in your base images and dependencies. The lifecycle policy prevents ECR from accumulating hundreds of old images and running up storage costs.

Step 4: Write the GitHub Actions Workflow

This is the complete workflow file. Create it at .github/workflows/deploy.yml:

name: Build and Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required for OIDC
  contents: read

env:
  AWS_REGION: ap-south-1
  ECR_REPOSITORY: my-app
  ECS_CLUSTER: my-cluster
  ECS_SERVICE: my-service

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        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 Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag, and push image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Wait for image scan
        run: |
          aws ecr wait image-scan-complete \
            --repository-name $ECR_REPOSITORY \
            --image-id imageTag=${{ github.sha }}

      - name: Check scan results
        run: |
          CRITICAL=$(aws ecr describe-image-scan-findings \
            --repository-name $ECR_REPOSITORY \
            --image-id imageTag=${{ github.sha }} \
            --query 'imageScanFindings.findingSeverityCounts.CRITICAL' \
            --output text)
          if [ "$CRITICAL" != "None" ] && [ "$CRITICAL" != "0" ]; then
            echo "CRITICAL vulnerabilities found: $CRITICAL"
            exit 1
          fi

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster $ECS_CLUSTER \
            --service $ECS_SERVICE \
            --force-new-deployment

The permissions: id-token: write block is essential. Without it, GitHub will not generate the OIDC token and the AWS credentials step will fail with a confusing error about missing tokens.

Step 5: Restrict Access by Branch

You can create separate roles for different environments:

# Production — only main branch
"token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:ref:refs/heads/main"

# Staging — only develop branch
"token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:ref:refs/heads/develop"

# Any branch (for building/testing only, not deploying)
"token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:*"

The production deployment role should have the strictest condition — only main branch. The staging role can allow the develop branch. A build-only role with limited permissions (just ECR push) can allow any branch.

Common Mistakes to Avoid

  1. Forgetting the permissions block in the workflow. Without id-token: write, the OIDC token is never generated. The error message does not clearly tell you this is the problem.
  2. Wrong audience value. The client_id_list in the OIDC provider must be sts.amazonaws.com. Some older tutorials use a different value that no longer works.
  3. Too broad IAM trust policy. Using repo:your-org/your-repo:* for a production deployment role means any branch, any PR, and any tag can deploy. Lock it down to the specific branch.
  4. Not restricting by repository. If you use a wildcard for the repository name in the trust policy, any repo in your GitHub organization could assume the role. Always specify the exact repo.
  5. Skipping image scanning. ECR scanning is free and catches known CVEs in your base images. There is no reason not to enable it. Failing the pipeline on critical vulnerabilities prevents you from deploying known-vulnerable images.

Frequently Asked Questions

What is OIDC and why should I use it instead of access keys?

OIDC lets GitHub Actions prove its identity to AWS without stored credentials. Instead of long-lived access keys in GitHub secrets, GitHub requests temporary credentials from AWS STS for each workflow run. These expire automatically when the job finishes. No keys to rotate, no keys to leak.

Does GitHub Actions OIDC work with any AWS service?

Yes. Once GitHub assumes the IAM role via OIDC, it has whatever permissions that role grants. You can deploy to ECS, EKS, S3, Lambda, CloudFormation — the IAM policy controls what the workflow can do.

Can I restrict which branches can deploy to production?

Yes. The IAM trust policy includes a condition on the sub claim that specifies which branches are allowed. Set it to ref:refs/heads/main for production so only the main branch can deploy.

What happens if someone forks my repo?

They cannot assume your AWS role. The trust policy includes your specific repository name. A fork has a different name (their-username/repo-name), so it is blocked by the condition.

Do I still need to store any secrets in GitHub?

You do not need AWS access keys. You do need the AWS account ID and IAM role ARN in your workflow, but these are identifiers, not secrets. You can put them directly in the workflow YAML or as GitHub variables.


Skip the Manual Setup — Use the Terraform Module

The OIDC provider, IAM role, ECR repository, lifecycle policy, and scanning configuration — all of it is in one Terraform module.

module "cicd" {
  source = "github.com/akshayghalme/terraform-cicd-pipeline"

  app_name        = "my-app"
  github_repo     = "your-username/your-repo"
  allowed_branch  = "main"
}

Run terraform apply, copy the role ARN into your GitHub Actions workflow, and you are done. Zero access keys, full security.

Get the Terraform CI/CD Pipeline module on GitHub →

AG

Akshay Ghalme

AWS DevOps Engineer with 3+ years building production cloud infrastructure. AWS Certified Solutions Architect. Currently managing a multi-tenant SaaS platform serving 1000+ customers.

More Guides & Terraform Modules

Every guide comes with a matching open-source Terraform module you can deploy right away.