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.
sequenceDiagram participant Workflow as GitHub Workflow participant OIDCProvider as GitHub OIDC Provider participant AWSSTS as AWS STS participant AWSService as AWS Service Note over Workflow: Request ID token from execution environment Workflow->>OIDCProvider: Request with repository information OIDCProvider-->>Workflow: Issue ID token in JWT format Workflow->>AWSSTS: AssumeRoleWithWebIdentity (with ID token) AWSSTS->>OIDCProvider: Retrieve JWKs AWSSTS->>AWSSTS: Verify JWT signature, iss, aud, exp, sub, etc. AWSSTS->>AWSSTS: Match against IAM role trust policy AWSSTS-->>Workflow: Return temporary credentials Workflow->>AWSService: Access with temporary credentials AWSService-->>Workflow: Response
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(()) }