Deploying to AWS from GitHub Actions with OIDC Authentication
Deploying to AWS from GitHub Actions with OIDC Authentication
Introduction
When deploying to AWS resources from GitHub Actions, the traditional approach has been to store IAM user access keys in GitHub Secrets. However, this method has several challenges:
- Risk of access key leakage
- Need for regular rotation work
- Increase in secrets to manage
OIDC (OpenID Connect) authentication solves these challenges. This article provides a detailed explanation of how to set up OIDC integration between GitHub Actions and AWS.
What is OIDC?
OIDC is an authentication protocol based on OAuth 2.0. GitHub Actions issues a token that can prove "I am a workflow from this repository" to AWS, and AWS verifies it and provides temporary credentials.
Benefits of OIDC
- No secrets required: No need to store access keys in GitHub
- Enhanced security: Uses only temporary credentials
- Fine-grained permission control: Permissions can be restricted by repository or branch
- Reduced management costs: No need for key rotation
AWS Configuration
1. Creating an IAM Identity Provider
First, create an IAM Identity Provider in the AWS Management Console.
- Open the IAM console
- Click "Identity providers" → "Add provider"
- Enter the following information:
Provider type: OpenID Connect
Provider URL: https://token.actions.githubusercontent.com
Audience: sts.amazonaws.com
Example with Terraform
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
"sts.amazonaws.com",
]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
}
Example with AWS SAM
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GitHub Actions OIDC Provider
Resources:
GitHubOIDCProvider:
Type: AWS::IAM::OIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
ThumbprintList:
- 6938fd4d98bab03faadb97b34396831e3780aea1
2. Creating an IAM Role
Next, create an IAM role that GitHub Actions will assume.
Configuring the Trust Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012: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:your-org/your-repo:*"
}
}
}
]
}
Condition Details
You can restrict access using the token.actions.githubusercontent.com:sub value.
# Entire specific repository
repo:your-org/your-repo:*
# Specific branch only
repo:your-org/your-repo:ref:refs/heads/main
# Specific environment only
repo:your-org/your-repo:environment:production
# Pull requests only
repo:your-org/your-repo:pull_request
Example with Terraform
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:your-org/your-repo:*"]
}
}
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-deploy-role"
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}
# Attach required permission policies
resource "aws_iam_role_policy_attachment" "deploy_policy" {
role = aws_iam_role.github_actions.name
policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess"
}
Example with AWS SAM
Resources:
GitHubActionsRole:
Type: AWS::IAM::Role
Properties:
RoleName: github-actions-deploy-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: !GetAtt GitHubOIDCProvider.Arn
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
token.actions.githubusercontent.com:sub: repo:your-org/your-repo:*
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECS_FullAccess
Outputs:
GitHubActionsRoleArn:
Description: ARN of the GitHub Actions IAM Role
Value: !GetAtt GitHubActionsRole.Arn
Export:
Name: GitHubActionsRoleArn
To deploy with SAM:
sam build
sam deploy --guided
After the initial deployment, register the output role ARN in GitHub Secrets as AWS_ROLE_ARN.
3. Attaching Permission Policies
Attach the necessary permissions for deployment to the role. For example:
- For S3 deployment:
AmazonS3FullAccess - For ECS deployment:
AmazonECS_FullAccess - For Lambda:
AWSLambda_FullAccess
For production environments, we recommend creating custom policies based on the principle of least privilege.
GitHub Actions Configuration
Creating a Workflow File
Create .github/workflows/deploy.yml.
name: Deploy to AWS
on:
push:
branches:
- main
# Permission settings to obtain OIDC token (Important!)
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-role
aws-region: ap-northeast-1
- name: Deploy to S3
run: |
aws s3 sync ./dist s3://your-bucket-name --delete
- name: Verify deployment
run: |
aws s3 ls s3://your-bucket-name
Important Points
1. permissions Configuration
permissions:
id-token: write # Required to obtain OIDC token
contents: read # Required to checkout repository
Without this setting, you won't be able to obtain the OIDC token and will get an error.
2. configure-aws-credentials Action Version
Use v4 or later. Older versions may not support OIDC.
uses: aws-actions/configure-aws-credentials@v4
Practical Example: Deploying a SAM Application
Here's an example of deploying a serverless application using AWS SAM.
name: Deploy SAM Application
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-northeast-1
SAM_STACK_NAME: my-sam-app
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: SAM Build
run: sam build --use-container
- name: SAM Deploy
run: |
sam deploy \
--stack-name ${{ env.SAM_STACK_NAME }} \
--capabilities CAPABILITY_IAM \
--resolve-s3 \
--no-fail-on-empty-changeset \
--no-confirm-changeset
- name: Get Stack Outputs
run: |
aws cloudformation describe-stacks \
--stack-name ${{ env.SAM_STACK_NAME }} \
--query 'Stacks[0].Outputs' \
--output table
Practical Example: Deploying to ECS
As a more practical example, here's an ECS deployment workflow.
name: Deploy to ECS
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-northeast-1
ECR_REPOSITORY: my-app
ECS_SERVICE: my-service
ECS_CLUSTER: my-cluster
CONTAINER_NAME: my-container
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push image to ECR
id: build-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
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Download task definition
run: |
aws ecs describe-task-definition \
--task-definition my-task \
--query taskDefinition > task-definition.json
- name: Update task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
Troubleshooting
Error: "Not authorized to perform sts:AssumeRoleWithWebIdentity"
Cause: Trust policy conditions don't match
Solution:
- Verify the
token.actions.githubusercontent.com:subvalue in the IAM role's trust policy - Check that the repository name and branch name are correct
- Check the actual
subclaim value in the GitHub Actions logs
- name: Debug OIDC token
run: |
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | \
jq -R 'split(".") | .[1] | @base64d | fromjson'
Error: "Error: Credentials could not be loaded"
Cause: Missing permissions configuration
Solution: Add the following to your workflow file
permissions:
id-token: write
contents: read
Authentication Succeeds but Permission Errors Occur
Cause: Required permission policies are not attached to the IAM role
Solution: Attach appropriate policies to the IAM role
aws iam attach-role-policy \
--role-name github-actions-deploy-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
Security Best Practices
1. Principle of Least Privilege
Grant only the minimum necessary permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-specific-bucket",
"arn:aws:s3:::your-specific-bucket/*"
]
}
]
}
2. Restrict by Branch or Tag
Restrict production deployments to specific branches only.
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
3. Separate Roles by Environment
Use different roles for development, staging, and production.
- name: Configure AWS credentials (Production)
if: github.ref == 'refs/heads/main'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_PROD }}
aws-region: ap-northeast-1
- name: Configure AWS credentials (Development)
if: github.ref == 'refs/heads/develop'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEV }}
aws-region: ap-northeast-1
4. Audit with CloudTrail
Record all API calls and review them regularly.
# S3 bucket for CloudTrail
resource "aws_s3_bucket" "cloudtrail" {
bucket = "my-cloudtrail-logs-bucket"
}
resource "aws_s3_bucket_policy" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AWSCloudTrailAclCheck"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:GetBucketAcl"
Resource = aws_s3_bucket.cloudtrail.arn
},
{
Sid = "AWSCloudTrailWrite"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.cloudtrail.arn}/*"
Condition = {
StringEquals = {
"s3:x-amz-acl" = "bucket-owner-full-control"
}
}
}
]
})
}
# CloudTrail
resource "aws_cloudtrail" "github_actions_audit" {
name = "github-actions-audit"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
include_global_service_events = true
is_multi_region_trail = true
enable_logging = true
depends_on = [aws_s3_bucket_policy.cloudtrail]
}
Conclusion
OIDC integration between GitHub Actions and AWS provides the following benefits:
- Secure deployment without access keys
- Uses only temporary credentials
- Fine-grained permission control by repository or branch
- Reduced management costs
While the initial setup is somewhat complex, once configured, it leads to long-term improvements in security and operability. I highly recommend considering its adoption.