Skip to main content
Security Modules 1.5.1Last updated in version 1.3.0

IAM Role for GitHub Actions

View Source Release Notes

This Terraform module can be used to create Assume Role policies and IAM Roles such that they can be used with GitHub Actions. This requires you to provision an IAM OpenID Connect Provider for GitHub Actions in your account. By using OpenID Connect, GitHub Actions can directly exchange credentials to access AWS without having to store and provide GitHub with permanent AWS access credentials. This is useful to prevent credential leaks from progressing undetected.

You can either use the account-baseline-app or account-baseline-security modules (setting enable_github_actions_access = true) or by adding the following resource to your Terraform module:

resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.oidc_thumbprint.certificates[0].sha1_fingerprint]
}

data "tls_certificate" "oidc_thumbprint" {
url = "https://token.actions.githubusercontent.com"
}

Creating the IAM Role

module "iam_role" {
# Update <VERSION> with latest version of the module
source = "git::git@github.com:gruntwork-io/terraform-aws-security.git//modules/github-actions-iam-role?ref=<VERSION>"

github_actions_openid_connect_provider_arn = aws_iam_openid_connect_provider.github_actions.arn
github_actions_openid_connect_provider_url = aws_iam_openid_connect_provider.github_actions.url

allowed_sources = {
"gruntwork-io/terraform-aws-security" = ["main"]
}

iam_role_name = "example-iam-role"
permitted_full_access_services = ["ec2"]
}

Security Considerations

The above example will configure the IAM role example-iam-role such that it is available to be assumed by GitHub Actions if it is run from the main branch of the gruntwork-io/terraform-aws-security repository. The IAM role would then have the ability to call any API in the ec2 namespace.

You can further customize the IAM role using the following variables:

  • permitted_full_access_services: List of AWS services that the IAM role will have full access to (set to SERVICE_NAME:*).
  • iam_policy: IAM policy statements that should be directly attached (inline) to the IAM role.
  • iam_policy_arns: List of IAM Policy ARNs that should be attached to the IAM role.
  • iam_customer_managed_policy_names: List of customer managed IAM policies that should be attached to the IAM role.
  • iam_aws_managed_policy_names: List of AWS managed IAM policies that should be attached to the IAM role.

Extend the allowed_sources map if you want to allow additional repositories or branches. For example, if you want to allow the dev branch on terraform-aws-security, as well as the main branch from terraform-aws-service-catalog:

module "iam_role" {
# Update <VERSION> with latest version of the module
source = "git::git@github.com:gruntwork-io/terraform-aws-security.git//modules/github-actions-iam-role?ref=<VERSION>"

github_actions_openid_connect_provider_arn = aws_iam_openid_connect_provider.github_actions.arn
github_actions_openid_connect_provider_url = aws_iam_openid_connect_provider.github_actions.url

allowed_sources = {
"gruntwork-io/terraform-aws-security" = ["main", "dev"]
"gruntwork-io/terraform-aws-service-catalog" = ["main"]
}

iam_role_name = "example-iam-role"
permitted_full_access_services = ["ec2"]
}

You can also use the module to only manage the assume role policy. This is useful if you want more control over the IAM role creation. For example:

module "assume_role_policy" {
# Update <VERSION> with latest version of the module
source = "git::git@github.com:gruntwork-io/terraform-aws-security.git//modules/github-actions-iam-role?ref=<VERSION>"

github_actions_openid_connect_provider_arn = aws_iam_openid_connect_provider.github_actions.arn
github_actions_openid_connect_provider_url = aws_iam_openid_connect_provider.github_actions.url

allowed_sources = {
"gruntwork-io/terraform-aws-security" = ["main", "dev"]
"gruntwork-io/terraform-aws-service-catalog" = ["main"]
}

create_iam_role = false
}

resource "aws_iam_role" "example" {
name = "example-iam-role"
assume_role_policy = module.assume_role_policy.assume_role_policy_json
}

Immutable subject claims (protecting against repo recycling)

GitHub Actions OIDC lets a repository opt in to embedding numeric, immutable owner/repo IDs in the sub claim (repo:org@owner_id/repo-name@repo_id:...) instead of just names (repo:org/repo-name:...). This closes a gap where a repository is deleted and a new, unrelated repository is created under the same name: with the name-only claim, the new repository would inherit trust intended for the old one. See the GitHub changelog post for details on enabling this on the GitHub side.

After July 15, 2026, you can only opt in to immutable subject claims on repositories that already existed before that date.

Once a repository has opted in, switch its key in allowed_sources from "org/repo-name" to "org@owner_id/repo-name@repo_id":

module "iam_role" {
# Update <VERSION> with latest version of the module
source = "git::git@github.com:gruntwork-io/terraform-aws-security.git//modules/github-actions-iam-role?ref=<VERSION>"

github_actions_openid_connect_provider_arn = aws_iam_openid_connect_provider.github_actions.arn
github_actions_openid_connect_provider_url = aws_iam_openid_connect_provider.github_actions.url

allowed_sources = {
# This repo has opted in to immutable subject claims, so its key uses the org@owner_id/repo-name@repo_id format.
"gruntwork-io@1816772/terraform-aws-security@22069063" = ["main"]
# This repo hasn't opted in yet, so it keeps the name-only format.
"gruntwork-io/terraform-aws-service-catalog" = ["main"]
}

iam_role_name = "example-iam-role"
permitted_full_access_services = ["ec2"]
}

Only repos that have opted in need the @owner_id/@repo_id suffixes; every other key keeps the name-only format, and a mix of both formats in the same allowed_sources map is fine. The module builds the exact numeric ID into the policy condition for any key that includes it, so migrating a repo is a one-line change to its key -- it doesn't add new repos or branches, it only changes the claim format used for that entry.

You can look up the numeric owner/repo IDs for a repository with the GitHub CLI:

gh api repos/{owner}/{repo} --jq '{owner_id: .owner.id, repo_id: .id}'

This works whether the owner is an organization or a personal account, so there's no separate lookup needed for either case.

Using created IAM Role in GitHub Actions Workflow

To use the created IAM role in your GitHub Actions Workflow, you need to configure the following:

  1. Attach permissions to write id-token at the workflow or job level. To add this permission, add the following as a top level key, or under the job:

    permissions: id-token: write

  2. Add the aws-actions/configure-aws-credentials to your step. This retrieves a JWT from the GitHub OIDC provider, and then requests an access token from AWS. For more information, see the AWS documentation.

       - name: configure aws credentials
uses: aws-actions/configure-aws-credentials@master
with:
role-to-assume: IAM_ROLE_ARN_OUTPUT_OF_MODULE
role-session-name: ANY_CUSTOM_SESSION_NAME
aws-region: DEFAULT_REGION_TO_USE

With these two settings, your GitHub Action should now be able to assume the IAM role you created in this module. The following is a full GitHub Action Workflow example that lists EC2 instances on every push:

# Sample workflow to access AWS resources when workflow is tied to branch
name: AWS example workflow
on:
push
permissions:
id-token: write
jobs:
ReadEC2Instances:
runs-on: ubuntu-latest
steps:
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@master
with:
role-to-assume: IAM_ROLE_ARN_OUTPUT_OF_MODULE
role-session-name: ANY_CUSTOM_SESSION_NAME
aws-region: us-east-1
- name: Whoami?
run: |
aws sts get-caller-identity
- name: Read EC2 Instances
run: |
aws ec2 describe-instances

Sample Usage

main.tf

# ------------------------------------------------------------------------------------------------------
# DEPLOY GRUNTWORK'S GITHUB-ACTIONS-IAM-ROLE MODULE
# ------------------------------------------------------------------------------------------------------

module "github_actions_iam_role" {

source = "git::git@github.com:gruntwork-io/terraform-aws-security.git//modules/github-actions-iam-role?ref=v1.5.1"

# ----------------------------------------------------------------------------------------------------
# REQUIRED VARIABLES
# ----------------------------------------------------------------------------------------------------

# Map of github repositories to the list of branches that are allowed to assume the IAM role. Each key can be
# encoded in one of two formats:
# - "org/repo-name" (e.g., gruntwork-io/terraform-aws-ci) - the standard, name-only format.
# - "org@owner_id/repo-name@repo_id" (e.g., gruntwork-io@1816772/terraform-aws-ci@22069063) - GitHub's
# immutable subject-claim format, using the numeric, immutable owner/repo IDs instead of names. Use this
# format only for repositories that have opted in to immutable subject claims for Actions OIDC tokens - see
# https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/. You can
# look up the numeric IDs for a repository with:
# gh api repos/{owner}/{repo} --jq '{owner_id: .owner.id, repo_id: .id}'
# A mix of both formats across different keys is fine; each key's format only affects that key's own entry.
#
allowed_sources = <map(list(string))>

# ----------------------------------------------------------------------------------------------------
# OPTIONAL VARIABLES
# ----------------------------------------------------------------------------------------------------

# List of additional thumbprints for the OIDC provider.
additional_thumbprints = null

# The string operator to use when evaluating the AWS IAM condition for
# determining which GitHub repos are allowed to assume the IAM role. Examples:
# StringEquals, StringLike, etc.
allowed_sources_condition_operator = "StringEquals"

# Whether to create the IAM role and attach permissions for GitHub Actions to
# assume.
create_iam_role = true

# Flag to enable/disable the creation of the GitHub OIDC provider.
create_oidc_provider = false

# The name to use for the custom inline IAM policy that is attached to the
# Role/Group when var.iam_policy is configured.
custom_iam_policy_name = "GrantCustomIAMPolicy"

# ARN of the OpenID Connect Provider provisioned for GitHub Actions.
github_actions_openid_connect_provider_arn = ""

# URL of the OpenID Connect Provider provisioned for GitHub Actions.
github_actions_openid_connect_provider_url = ""

# A list of IAM AWS Managed Policy names to attach to the group.
iam_aws_managed_policy_names = null

# A list of IAM AWS Customer Managed policy names to attach to the group.
iam_customer_managed_policy_names = null

# An object defining the IAM policy statements that should be attached
# directly to the IAM role/group. The input is a map of objects where the map
# keys are SIDs for IAM policy statements, and the object fields are the
# resources, actions, and the effect of the statement.
iam_policy = {}

# A list of policies (by ARN) to attach to this group.
iam_policy_arns = null

# The name of an IAM role to create. Required when var.create_iam_role is
# true.
iam_role_name = null

# The maximum allowable session duration, in seconds, for the credentials you
# get when assuming the IAM roles created by this module.
max_session_duration = 43200

# A list of AWS services for which the IAM role will receive full permissions.
# See
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actions-resources-contextkeys.html
# to find the service name. For example, to grant developers access only to
# EC2 and Amazon Machine Learning, use the value ["ec2","machinelearning"].
permitted_full_access_services = []

}


Reference

Required

allowed_sourcesmap(list(…))required

Map of github repositories to the list of branches that are allowed to assume the IAM role. Each key can be encoded in one of two formats:

  • 'org/repo-name' (e.g., gruntwork-io/terraform-aws-ci) - the standard, name-only format.
  • 'org@owner_id/repo-name@repo_id' (e.g., gruntwork-io@1816772/terraform-aws-ci@22069063) - GitHub's immutable subject-claim format, using the numeric, immutable owner/repo IDs instead of names. Use this format only for repositories that have opted in to immutable subject claims for Actions OIDC tokens - see https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/. You can look up the numeric IDs for a repository with: gh api repos/{owner}/{repo} --jq '{owner_id: .owner.id, repo_id: .id}' A mix of both formats across different keys is fine; each key's format only affects that key's own entry.
map(list(string))

Optional

additional_thumbprintslist(string)optional

List of additional thumbprints for the OIDC provider.

null

The string operator to use when evaluating the AWS IAM condition for determining which GitHub repos are allowed to assume the IAM role. Examples: StringEquals, StringLike, etc.

"StringEquals"
create_iam_rolebooloptional

Whether to create the IAM role and attach permissions for GitHub Actions to assume.

true

Flag to enable/disable the creation of the GitHub OIDC provider.

false

The name to use for the custom inline IAM policy that is attached to the Role/Group when iam_policy is configured.

"GrantCustomIAMPolicy"

ARN of the OpenID Connect Provider provisioned for GitHub Actions.

""

URL of the OpenID Connect Provider provisioned for GitHub Actions.

""
iam_aws_managed_policy_nameslist(string)optional

A list of IAM AWS Managed Policy names to attach to the group.

null

A list of IAM AWS Customer Managed policy names to attach to the group.

null
iam_policymap(object(…))optional

An object defining the IAM policy statements that should be attached directly to the IAM role/group. The input is a map of objects where the map keys are SIDs for IAM policy statements, and the object fields are the resources, actions, and the effect of the statement.

map(object({
resources = list(string)
actions = list(string)
effect = string
}))
{}
Example
     iam_policy = {
S3Access = {
actions = ["s3:*"]
resources = ["arn:aws:s3:::mybucket"]
effect = "Allow"
},
EC2Access = {
actions = ["ec2:*"]
resources = ["*"]
effect = "Allow"
}
}

iam_policy_arnslist(string)optional

A list of policies (by ARN) to attach to this group.

null
iam_role_namestringoptional

The name of an IAM role to create. Required when create_iam_role is true.

null
max_session_durationnumberoptional

The maximum allowable session duration, in seconds, for the credentials you get when assuming the IAM roles created by this module.

43200
permitted_full_access_serviceslist(string)optional

A list of AWS services for which the IAM role will receive full permissions. See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actions-resources-contextkeys.html to find the service name. For example, to grant developers access only to EC2 and Amazon Machine Learning, use the value ['ec2','machinelearning'].

[]