iTranslated by AI
Secure AWS Deployment with Terraform and GitHub OIDC
Introduction
When deploying to AWS from GitHub Actions, the first challenge you face is "how to handle authentication."
While putting IAM user access keys into GitHub Secrets is simple, it carries the risk of storing long-term credentials in a repository. Managing key rotation is also a bit of a hassle.
By using GitHub OIDC (OpenID Connect), you can access AWS without long-term credentials. Furthermore, managing that configuration with Terraform ensures reproducibility and visibility of your settings.
In this article, I will introduce the GitHub OIDC × Terraform design that I actually built for my personal development project, the diary app Storyie.
Why OIDC?
Let's clarify the differences between the traditional method and OIDC.
IAM User + Access Key Method
- Register
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin GitHub Secrets - If the keys are leaked, permanent access is possible
- Periodic rotation is required (but often forgotten)
GitHub OIDC Method
- Authenticate to AWS using short-lived tokens issued by GitHub
- No long-term credentials exist
- Access can be restricted per branch or repository
- Validate "which request comes from which branch of which repository" using the trust policy on the AWS side
Even in personal development, I don't want to cut corners on security. With OIDC, the configuration cost isn't that high, and once it's built, you're freed from the burden of rotation management.
Overall Architecture
The structure is simple.
terraform/github-oidc/
├── main.tf # OIDC Provider + IAM Role + Policy
├── variables.tf # Variable definitions
├── outputs.tf # Outputs (Role ARN, etc.)
└── terraform.tfvars.example # Configuration example
There are three resources managed by Terraform:
- OIDC Provider — Register GitHub Actions as a trusted identity provider in AWS
- IAM Role — The target that GitHub Actions assumes. Restricted by repository and branch in the trust policy
- IAM Policy — A collection of AWS permissions required for SST deployment
Terraform Design
OIDC Provider
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
"sts.amazonaws.com",
]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
}
Specify sts.amazonaws.com in client_id_list. This is the audience used when GitHub Actions performs the token exchange with AWS STS.
The thumbprint_list is the fingerprint of the TLS certificate for GitHub's OIDC endpoint. AWS uses it to verify the token from GitHub.
Trust Policy — Which Repositories and Branches to Allow
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
actions = ["sts:AssumeRoleWithWebIdentity"]
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = [
for branch in var.github_branches :
"repo:${var.github_org}/${var.github_repo}:ref:refs/heads/${branch}"
]
}
}
}
This is the core of security. By using the conditions on the sub claim, only requests from specific branches of specific repositories are permitted.
In Storyie, only the main and develop branches are allowed. Since direct deployments from feature branches are rejected by the trust policy, you can enforce the deployment flow.
Because it is managed with variables, adding or changing branches is as simple as editing terraform.tfvars and running apply.
variable "github_branches" {
description = "List of GitHub branches allowed to assume the role"
type = list(string)
default = ["develop", "main"]
}
IAM Policy for SST Deployment
SST (Serverless Stack) operates many AWS services internally, such as CloudFormation, S3, Lambda, and CloudFront. The necessary permissions are consolidated into a policy document.
data "aws_iam_policy_document" "sst_deploy" {
# CloudFormation — SST stack management
statement {
sid = "CloudFormation"
effect = "Allow"
actions = ["cloudformation:*"]
resources = ["*"]
}
# S3 — Asset delivery and SST state management
statement {
sid = "S3"
effect = "Allow"
actions = ["s3:*"]
resources = ["*"]
}
# Lambda — Next.js Server Functions
statement {
sid = "Lambda"
effect = "Allow"
actions = ["lambda:*"]
resources = ["*"]
}
# CloudFront — CDN delivery
# Route53 — DNS management
# ACM — SSL certificates
# SQS + DynamoDB — Next.js ISR Revalidation
# ... same for other services
}
To be honest, frequent use of * is a tradeoff for personal development. In a corporate project, you should be more strict by narrowing down the resource ARNs. However, in personal development, SST may create new resources with each version update, so I balance this with the stress of deployment failures caused by insufficient permissions.
GitHub Actions Configuration
Next, use the role created with Terraform in GitHub Actions.
jobs:
deploy:
runs-on: self-hosted
permissions:
id-token: write # Required to obtain the OIDC token
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Deploy to AWS with SST
run: pnpm sst:deploy:prod
There are two key points.
permissions.id-token: write: Without this, GitHub will not issue an OIDC token. Since id-token permissions are not granted by default, you must specify this explicitly.
aws-actions/configure-aws-credentials@v4: By simply passing the Role ARN to role-to-assume, it internally handles everything from obtaining the OIDC token to executing AssumeRoleWithWebIdentity for STS and setting up temporary credentials.
Since AWS_ROLE_ARN is shown in the Terraform output, register it in GitHub Secrets.
Setup Procedure
1. Run Terraform
cd terraform/github-oidc
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars (set github_org, github_repo)
terraform init
terraform plan
terraform apply
2. Configure GitHub Secrets
Register the Role ARN displayed in the output of terraform apply as AWS_ROLE_ARN in the GitHub repository's Secrets.
The Terraform outputs also include the setup instructions.
output "github_secret_instructions" {
value = <<-EOT
1. Copy the role ARN above
2. Go to Settings → Secrets and variables → Actions
3. Name: AWS_ROLE_ARN
4. Value: (Role ARN)
EOT
}
It is quite convenient to embed the steps in the output so that team members or your future self don't get lost.
3. Run the Workflow
Now, simply push to main or develop, and the automatic deployment will run via OIDC authentication.
State Management
In this setup, the Terraform state is saved locally.
backend "local" {
path = "terraform.tfstate"
}
Since OIDC settings are rarely changed once created, I decided that building a remote backend with S3 + DynamoDB was not worth the effort. Since I am the only one working on this personal development project, state conflicts do not occur.
However, don't forget to add terraform.tfstate to your .gitignore. The state file contains information like AWS Account IDs and Role ARNs.
Insights from Actual Operation
What Went Well
- Zero Rotation Management: Once built, you can completely forget about it. This is the biggest advantage.
- Branch Restrictions as Natural Guardrails: I no longer accidentally deploy from feature branches.
-
Visibility via Terraform: Even six months later, I can understand the role's permissions just by looking at the
.tffiles. - Setup Procedures in Outputs: I don't get lost when creating the same configuration for a new repository.
Points to Note
-
Forgetting
id-token: write: This was the first point I got stuck on. Without it, the OIDC token cannot be retrieved, resulting inAccessDenied. - Thumbprint Updates: If GitHub updates its certificate, the thumbprint may change. Check this if you encounter errors.
- Combination with Self-hosted Runners: Storyie uses self-hosted runners, but OIDC works the same way whether you use GitHub-hosted or self-hosted runners.
Conclusion
The combination of GitHub OIDC + Terraform is an investment that pays off even for personal development.
- Free yourself from managing long-term credentials.
- Control deployment permissions at the branch level.
- Make configurations reproducible with Terraform.
Because personal development is where security tends to be "postponed due to hassle," solving it with a system at the beginning allows you to focus on your code with peace of mind.
The configuration introduced in this article is the actual deployment infrastructure for the diary app Storyie.
📝 Web: https://storyie.com
🍎 iOS: App Store
🤖 Android Beta: https://storyie.com/android-beta
Discussion