Best practices for securely assuming AWS IAM roles from GitHub Actions
When building CI/CD with GitHub Actions, there are times when you want to access AWS resources, right? In such cases, it is recommended to assume an IAM role through a flow that uses OpenID Connect (OIDC).
This article will introduce the flow up to assuming an IAM role and the specific steps involved!
Methods for Assuming Roles#
First, when considering how to assume an IAM role, it's necessary to understand all the available options. Methods for assuming IAM roles are documented in the AWS documentation.
Among these, OpenID Connect (OIDC) is the most robust choice when assuming an IAM role from outside AWS.
Benefits of Using OIDC#
There are several important benefits to assuming AWS IAM roles from GitHub Actions using OpenID Connect (OIDC).
- No need to store secrets statically: There's no need to store long-term AWS access keys or secrets in GitHub secrets. This significantly reduces the risk of credential leakage.
- Automated credential rotation: When using OIDC, temporary credentials are automatically generated, eliminating the need for manual credential updates or rotation.
- Fine-grained access control: Using IAM trust policies, you can restrict access based on specific repositories, branches, tags, etc., enabling security design that follows the principle of least privilege.
- Improved auditability: All actions are associated with specific GitHub workflows, making logging and auditing easier in AWS CloudTrail.
- Simplified management: Instead of managing multiple service accounts or access keys, you only need to set up trust relationship-based integration.
Due to these benefits, OIDC has become the current best practice for integrating GitHub Actions with AWS.
This time, we will also use role assumption via OIDC.
Flow Until Role Assumption#
The role assumption procedure in GitHub Actions introduced here follows the same general flow as AWS IAM Web Identity.
Steps#
- Create Identity Provider
- Create IAM role
- Create GitHub Actions configuration file
Creating the Identity Provider#
Create a new Identity provider from IAM > Identity providers. The values to enter are fixed.
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
Creating the IAM Role#
When creating an IAM role from the Management Console, use the following configuration.
- Identity provider: Select the IdP you created
Once you select the IdP, you can enter claim information. The sub section provides input assistance separated into username, repository name, and branch name.
- Audience: Select
sts.amazonaws.com - GitHub organization: Organization name or username
- GitHub repository: Repository name (optional)
- GitHub branch: Branch name (optional)
The trust policy will look like this. Please use this as a reference when creating with IaC.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::{account}:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:{user_or_org}/{repo}:*"
}
}
}
]
}Creating GitHub Workflows#
By using aws-actions/configure-aws-credentials, you can automatically retrieve credentials and assume the role.
Note that this example includes GitHub Actions for Rust because test code is prepared in Rust this time.
name: Fetch SSM Parameter
on:
workflow_dispatch:
env:
AWS_REGION: "ap-northeast-1"
permissions:
id-token: write
contents: read
jobs:
test:
name: Unit Test
runs-on: ubuntu-latest
steps:
- name: Clone Repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.IAM_ROLE_ARN }}
role-session-name: samplerolesession
aws-region: ${{ env.AWS_REGION }}
- name: Setup Rust Toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Run
run: cargo run#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let client = aws_sdk_ssm::Client::new(&config);
let value = client
.get_parameter()
.name("/message")
.send()
.await?
.parameter
.and_then(|p| p.value)
.unwrap_or_default();
println!("{}", value);
Ok(())
}