Skip to content
Back to docs
DevOps 12 min read

GitHub Actions OIDC with AWS

A practical guide to GitHub Actions OIDC providers, IAM role trust policies, deployment roles, and repository access patterns in AWS.

GitHub Actions can deploy to AWS without storing long-lived AWS access keys. It does this by using OpenID Connect (OIDC) to request short-lived AWS credentials.

The two AWS resources that make this work are:

  1. An IAM OIDC provider, which allows the AWS account to recognize identity tokens issued by GitHub.
  2. One or more IAM roles, which define which GitHub repositories or environments may access AWS and what they are allowed to do.

The most important rule is:

Create the GitHub OIDC provider once per AWS account, then create dedicated IAM roles for repositories, environments, and permission boundaries.


1. High-level architecture

GitHub Actions workflow
        |
        | Requests a short-lived OIDC token
        v
GitHub OIDC issuer
token.actions.githubusercontent.com
        |
        | Sends token to AWS STS
        v
AWS account-level OIDC provider
        |
        | AWS validates issuer and audience
        v
IAM role trust policy
        |
        | Validates repository, branch, tag,
        | environment, or reusable workflow
        v
AWS STS issues temporary credentials
        |
        | Credentials are limited by the
        | IAM role permission policies
        v
GitHub Actions deploys to AWS

No permanent AWS access key or secret access key needs to be stored in GitHub.


2. What is an IAM OIDC provider?

An IAM OIDC provider represents an external identity issuer that AWS trusts.

For GitHub Actions, the provider normally looks like this:

arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com

Example:

arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com

Its important configuration is:

Provider URL: token.actions.githubusercontent.com
Audience:     sts.amazonaws.com

The provider tells AWS:

Tokens issued by GitHub Actions for AWS STS are from a recognized identity source.

The provider does not contain:

  • A list of allowed repositories
  • Allowed branches
  • GitHub environment names
  • AWS deployment permissions
  • ECS, ECR, S3, or Terraform permissions

Those restrictions are configured on IAM roles.

One provider, many roles

A single provider can be shared by multiple roles:

GitHub OIDC provider

├── ops-pilot-api-github-plan-dev
├── ops-pilot-api-github-deploy-dev
├── ops-pilot-api-github-plan-prod
├── ops-pilot-api-github-deploy-prod
├── signal-tester-github-plan-dev
└── signal-tester-github-deploy-dev

You normally do not create a new provider when adding a repository.


3. What is an IAM role?

An IAM role is an AWS identity that can be assumed temporarily. Unlike an IAM user, a role does not have permanent access keys of its own.

For GitHub deployments, an IAM role has two distinct security layers:

Trust policy

The trust policy answers:

Who is allowed to assume this role?

For GitHub OIDC, it identifies the GitHub provider and restricts token claims such as repository and environment.

Permission policies

Permission policies answer:

What can the workflow do after assuming this role?

For example:

ecr:GetAuthorizationToken
ecr:PutImage
ecs:RegisterTaskDefinition
ecs:UpdateService
ecs:DescribeServices
iam:PassRole

A workflow must pass both checks:

  1. Its GitHub identity must satisfy the role trust policy.
  2. Its AWS action must be allowed by the role permission policies.

4. Provider versus role

ResourcePurposeTypical scope
IAM OIDC providerMakes AWS recognize GitHub as an identity issuerOnce per AWS account
IAM role trust policyDefines which GitHub identity may assume the roleRepository, environment, branch, tag, or workflow
IAM role permission policyDefines which AWS actions are allowedECS, ECR, Terraform, S3 state, IAM pass role, and similar
GitHub environmentAdds approvals, variables, secrets, and deployment restrictionsdev, staging, prod

A useful mental model is:

OIDC provider = AWS recognizes GitHub-issued ID cards

Role trust policy = The entry list for a particular room

Role permission policy = What a person may do inside that room

5. What happens during a GitHub deployment?

A typical deployment follows this sequence:

  1. A GitHub Actions job begins.
  2. The workflow requests an OIDC token from GitHub.
  3. GitHub issues a short-lived JSON Web Token.
  4. The token includes claims describing the workflow identity.
  5. aws-actions/configure-aws-credentials sends the token to AWS STS.
  6. AWS recognizes GitHub through the account-level OIDC provider.
  7. AWS evaluates the target IAM role’s trust policy.
  8. AWS confirms that the token’s audience and subject are allowed.
  9. AWS STS returns temporary credentials.
  10. The workflow uses those credentials until they expire.
  11. IAM permission policies determine which AWS operations succeed.

The AWS STS operation involved is:

sts:AssumeRoleWithWebIdentity

6. GitHub OIDC token subjects

The role trust policy commonly evaluates the token’s sub claim.

GitHub environment

When a workflow job uses a GitHub environment:

jobs:
  deploy:
    environment: dev

The subject normally resembles:

repo:thecultofdev/signal-tester:environment:dev

Branch

For a workflow tied directly to a branch, the subject can resemble:

repo:thecultofdev/signal-tester:ref:refs/heads/master

Tag

For a tag-triggered workflow:

repo:thecultofdev/signal-tester:ref:refs/tags/v1.0.0

Using a protected GitHub environment is usually preferable for Terraform apply, infrastructure destruction, and deployments because GitHub can require approvals and restrict which branches may use the environment.


7. Example trust policy for signal-tester

The following policy permits only the thecultofdev/signal-tester repository using the GitHub environment named dev:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::610452361839: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:thecultofdev/signal-tester:environment:dev"
        }
      }
    }
  ]
}

This policy answers only who may assume the role. It does not grant ECS or ECR access by itself.


8. Why the existing OpsPilot roles cannot be reused automatically

An OpsPilot role may contain a condition such as:

{
  "StringLike": {
    "token.actions.githubusercontent.com:sub": ["repo:thecultofdev/ops-pilot-api:environment:dev"]
  }
}

A token from signal-tester will have a different subject:

repo:thecultofdev/signal-tester:environment:dev

Because the values do not match, AWS denies the role assumption.

You could add both repositories to the same role, but both repositories would then receive the role’s AWS permissions. That creates unnecessary coupling.

A safer design is:

ops-pilot-api
    → ops-pilot-api role
    → OpsPilot resources

signal-tester
    → signal-tester role
    → Signal Tester resources

For a small sandbox application, use at least two roles:

signal-tester-github-plan-dev
signal-tester-github-deploy-dev

A stronger separation is:

signal-tester-github-plan-dev
signal-tester-github-terraform-dev
signal-tester-github-deploy-dev

Plan role

Used for Terraform validation and planning.

Typical access:

  • Read Terraform state
  • Read AWS resource metadata
  • Run Describe, List, and Get operations
  • Create and remove the S3 state lock file when necessary
  • No application deployment
  • No broad infrastructure mutation

Terraform role

Used for terraform apply and terraform destroy.

Typical access:

  • Create and modify signal-tester infrastructure
  • Manage ECS, ECR, ALB, CloudWatch, EventBridge, IAM roles, and security groups
  • Read and update Terraform state
  • Be restricted by resource names and tags where practical

Deploy role

Used for application image deployment.

Typical access:

  • Authenticate to ECR
  • Push image layers and image manifests
  • Read ECS task definitions
  • Register task definition revisions
  • Update the ECS service
  • Describe deployment status
  • Pass only the signal-tester task and execution roles

Terraform generally needs broader permissions than an application deployment. Separating them limits the effect of a compromised workflow.


10. GitHub Actions workflow configuration

A job using OIDC must request permission to issue an identity token:

permissions:
  contents: read
  id-token: write

Example deployment job:

name: Deploy

on:
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy Signal Tester
    runs-on: ubuntu-latest
    environment: dev

    permissions:
      contents: read
      id-token: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: ${{ vars.AWS_GITHUB_DEPLOY_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}

      - name: Confirm AWS identity
        run: aws sts get-caller-identity

The job uses:

environment: dev

because the IAM trust policy expects:

repo:thecultofdev/signal-tester:environment:dev

Without the matching GitHub environment, role assumption will fail.


11. GitHub variables versus secrets

IAM role ARNs are identifiers, not credentials.

Store these as GitHub repository or environment variables:

AWS_REGION
AWS_ACCOUNT_ID
AWS_GITHUB_PLAN_ROLE_ARN
AWS_GITHUB_TERRAFORM_ROLE_ARN
AWS_GITHUB_DEPLOY_ROLE_ARN
ECS_CLUSTER_NAME
ECS_SERVICE_NAME
ECR_REPOSITORY_NAME
TF_STATE_BUCKET
TF_STATE_KEY

Example:

AWS_GITHUB_DEPLOY_ROLE_ARN=
arn:aws:iam::610452361839:role/signal-tester-github-deploy-dev

Do not configure long-lived credentials when using OIDC:

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY

Application secrets, such as an internal API key, are separate from GitHub OIDC and should normally live in AWS Secrets Manager.


12. GitHub OIDC versus AWS Secrets Manager

These resources solve different problems.

GitHub OIDC role

Used by GitHub Actions to authenticate to AWS.

arn:aws:iam::610452361839:role/signal-tester-github-deploy-dev

Application secret

Used by the running ECS task.

arn:aws:secretsmanager:ap-southeast-1:610452361839:
secret:signal-tester/dev/internal-api-key-xxxxxx

The GitHub deployment workflow usually does not need the secret value.

Terraform only needs the secret ARN so it can configure the ECS task definition:

secrets = [
  {
    name      = "INTERNAL_API_KEY"
    valueFrom = var.internal_api_key_secret_arn
  }
]

At task startup, ECS retrieves the value through the ECS task execution role.

GitHub Actions OIDC
    → authenticates the deployment workflow

Secrets Manager
    → provides application secrets to the ECS task

They are not interchangeable.


13. Terraform example for a GitHub OIDC role

data "aws_iam_openid_connect_provider" "github" {
  arn = "arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com"
}

data "aws_iam_policy_document" "github_deploy_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type = "Federated"

      identifiers = [
        data.aws_iam_openid_connect_provider.github.arn
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"

      values = [
        "repo:thecultofdev/signal-tester:environment:dev"
      ]
    }
  }
}

resource "aws_iam_role" "github_deploy" {
  name               = "signal-tester-github-deploy-dev"
  assume_role_policy = data.aws_iam_policy_document.github_deploy_trust.json

  tags = {
    Application = "signal-tester"
    Environment = "dev"
    Purpose     = "github-actions-deployment"
    ManagedBy   = "terraform"
  }
}

The OIDC provider is referenced as an existing account-level resource. Terraform creates only the new repository-specific role.


14. Example deploy permission policy

The following is illustrative and must be adjusted to the exact ECS, ECR, and IAM resource ARNs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EcrAuthorization",
      "Effect": "Allow",
      "Action": ["ecr:GetAuthorizationToken"],
      "Resource": "*"
    },
    {
      "Sid": "PushSignalTesterImage",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:ap-southeast-1:610452361839:repository/signal-tester"
    },
    {
      "Sid": "DeploySignalTester",
      "Effect": "Allow",
      "Action": [
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition",
        "ecs:UpdateService"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassSignalTesterRoles",
      "Effect": "Allow",
      "Action": ["iam:PassRole"],
      "Resource": [
        "arn:aws:iam::610452361839:role/signal-tester-ecs-execution-dev",
        "arn:aws:iam::610452361839:role/signal-tester-ecs-task-dev"
      ]
    }
  ]
}

Some ECS actions do not support complete resource-level restriction in every context. Use condition keys, naming conventions, and narrowly scoped iam:PassRole wherever possible.


15. Verify the OIDC provider

List account OIDC providers:

aws iam list-open-id-connect-providers

Inspect the GitHub provider:

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn \
  arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com

Expected values include:

{
  "Url": "token.actions.githubusercontent.com",
  "ClientIDList": ["sts.amazonaws.com"]
}

You do not need to add a repository to this provider.


16. Find roles that use GitHub OIDC

for role in $(aws iam list-roles \
  --query "Roles[].RoleName" \
  --output text); do

  policy=$(aws iam get-role \
    --role-name "$role" \
    --query "Role.AssumeRolePolicyDocument" \
    --output json)

  if echo "$policy" | grep -q "token.actions.githubusercontent.com"; then
    echo
    echo "===== $role ====="
    echo "$policy" | jq .
  fi
done

Inspect a specific role:

aws iam get-role \
  --role-name signal-tester-github-deploy-dev \
  --query "Role.AssumeRolePolicyDocument" \
  --output json | jq .

17. Inspect role permissions

List managed policies:

aws iam list-attached-role-policies \
  --role-name signal-tester-github-deploy-dev

List inline policies:

aws iam list-role-policies \
  --role-name signal-tester-github-deploy-dev

Read an inline policy:

aws iam get-role-policy \
  --role-name signal-tester-github-deploy-dev \
  --policy-name <POLICY_NAME>

The trust policy and permission policies must be reviewed separately.


18. Test the role from GitHub Actions

Add a temporary verification step after configuring credentials:

- name: Confirm assumed AWS identity
  run: aws sts get-caller-identity

Expected output should contain an assumed-role ARN resembling:

arn:aws:sts::610452361839:
assumed-role/signal-tester-github-deploy-dev/<session-name>

It should not show an IAM user ARN.

Remove or retain this step according to your logging preferences. It does not reveal secret credentials.


19. Common errors

Not authorized to perform sts:AssumeRoleWithWebIdentity

Likely causes:

  • Repository name in sub does not match
  • GitHub environment name does not match
  • The workflow job does not declare the expected environment
  • Audience is not sts.amazonaws.com
  • The workflow lacks id-token: write
  • The wrong role ARN is configured
  • The role references the wrong OIDC provider

Workflow uses environment: dev, but trust policy allows a branch

These subjects are different:

repo:thecultofdev/signal-tester:environment:dev
repo:thecultofdev/signal-tester:ref:refs/heads/master

The role must allow the subject GitHub actually issues.

OIDC works, but deployment receives AccessDenied

OIDC authentication succeeded, but the role’s permission policies do not allow the requested AWS action.

Check the failing API operation and update only the necessary permissions.

iam:PassRole failure

The deployment role must be permitted to pass the ECS task role and execution role:

iam:PassRole

Restrict this permission to the exact roles used by the signal-tester task definition.

Existing role works for one repo but not another

The trust policy is probably restricted to the original repository. Create a dedicated role or deliberately add the new repository subject after reviewing the shared permissions.


20. Security recommendations

  1. Create the GitHub OIDC provider once per AWS account.
  2. Create dedicated roles per application and environment.
  3. Use protected GitHub environments for deployment and Terraform apply.
  4. Separate Terraform and application deployment permissions.
  5. Restrict trust policies to exact repositories and environments.
  6. Avoid organization-wide wildcard subjects.
  7. Restrict iam:PassRole to exact ECS roles.
  8. Use short-lived OIDC credentials instead of IAM user access keys.
  9. Keep application secrets in Secrets Manager or SSM Parameter Store.
  10. Review CloudTrail for AssumeRoleWithWebIdentity activity.
  11. Use IAM Access Analyzer to validate trust and permission policies.
  12. Pin third-party GitHub Actions to trusted versions or commit SHAs for stricter supply-chain controls.

For each new repository:

1. Keep the existing account-level GitHub OIDC provider.
2. Decide which GitHub environments will be used.
3. Create repository- and environment-specific IAM roles.
4. Configure each role's trust policy.
5. Attach least-privilege permission policies.
6. Add role ARNs as GitHub variables.
7. Add `id-token: write` to the workflow.
8. Configure `aws-actions/configure-aws-credentials`.
9. Run `aws sts get-caller-identity` to verify.
10. Test plan, apply, and deploy permissions separately.

For signal-tester:

Existing provider:
arn:aws:iam::610452361839:
oidc-provider/token.actions.githubusercontent.com

Recommended roles:
signal-tester-github-plan-dev
signal-tester-github-terraform-dev
signal-tester-github-deploy-dev

Allowed GitHub identity:
repo:thecultofdev/signal-tester:environment:dev

22. Final mental model

AWS account

├── OIDC provider: GitHub
│   ├── Recognizes GitHub-issued tokens
│   ├── Audience: sts.amazonaws.com
│   └── Shared by multiple IAM roles

├── IAM role: ops-pilot-api deploy dev
│   ├── Trusts the GitHub provider
│   ├── Allows ops-pilot-api/dev
│   └── Can deploy OpsPilot resources

└── IAM role: signal-tester deploy dev
    ├── Trusts the same GitHub provider
    ├── Allows signal-tester/dev
    └── Can deploy Signal Tester resources

The provider establishes trust with GitHub as a platform.

The IAM role establishes trust with a specific GitHub workload and defines its AWS permissions.


References