IAM Least Privilege: Roles, Policies, and OIDC
Authentication and Authorization are the ultimate guardrails of cloud infrastructure. In AWS, this is governed by Identity and Access Management (IAM).
Historically, developers connected CI/CD pipelines to AWS by generating a long-lived IAM User, downloading a static AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, and pasting them into GitHub Secrets.
This is a critical security vulnerability. If an attacker compromises your repository or gains access to your organization, they steal those static keys, resulting in a compromised cloud account and massive AWS bills.
A modern, production-grade DevOps workflow must:
- Adhere strictly to the Principle of Least Privilege.
- Eliminate static, long-lived credentials entirely by leveraging OpenID Connect (OIDC) to fetch temporary, short-lived security tokens.
Understanding IAM Architecture
An IAM configuration consists of three primary entities:
- IAM Policy: A JSON document that defines what actions are allowed or denied on what resources.
- IAM Role: An identity with permission policies that can be assumed by anyone who needs it (users, services, or external systems). It does not have static credentials.
- Trust Policy (AssumeRolePolicy): A mandatory policy attached to an IAM Role that defines who is allowed to assume the role.
+-----------------------+ +-----------------------+
| Trust Policy | | Permission Policy |
| "GitHub is allowed | ── Assume ─> "Can write to S3 |
| to assume this" | Role | and write state locks"|
+-----------------------+ +-----------------------+
Step 1: Configuring the OIDC Identity Provider in AWS
To connect GitHub Actions to AWS without keys, we must register GitHub as a trusted Identity Provider (IdP) in our AWS account.
# modules/iam/oidc.tf
# 1. Register GitHub as OIDC Identity Provider
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# Thumbprint of GitHub's OIDC certificate
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8516e8758ebb07f540b347f4958d56b4b"]
}
Step 2: Provisioning the Terraform Execution Role
Now, we create an IAM Role that our GitHub pipeline will assume. We must attach a trust policy that restricts access only to our specific GitHub repository.
# modules/iam/variables.tf
variable "github_org" {
description = "GitHub Organization or username"
type = string
}
variable "github_repo" {
description = "GitHub Repository name"
type = string
}
variable "environment" {
description = "Environment boundary (dev, prod)"
type = string
}
# modules/iam/main.tf
# Fetch AWS Account ID dynamically
data "aws_caller_identity" "current" {}
# 1. Define Trust Policy allowing GitHub to assume role via OIDC
data "aws_iam_policy_document" "github_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
# Restrict assumption to our specific GitHub Repository and Branch
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
# Format: repo:org/repo:ref:refs/heads/branch
values = ["repo:${var.github_org}/${var.github_repo}:*"]
}
}
}
# 2. Create the IAM Role
resource "aws_iam_role" "terraform_execution" {
name = "github-terraform-execution-role-${var.environment}"
assume_role_policy = data.aws_iam_policy_document.github_trust.json
tags = {
Environment = var.environment
}
}
Step 3: Granting Permissions (Least Privilege Policy)
Now we define a permission policy that grants our Terraform role access to manage only the state S3 bucket, DynamoDB table, and basic resources in our account.
# modules/iam/main.tf (continued)
# Define Permissions for Terraform execution role
data "aws_iam_policy_document" "terraform_permissions" {
# Allow full management of the state bucket
statement {
effect = "Allow"
actions = ["s3:*"]
resources = [
"arn:aws:s3:::codesprintpro-tfstate-${data.aws_caller_identity.current.account_id}",
"arn:aws:s3:::codesprintpro-tfstate-${data.aws_caller_identity.current.account_id}/*"
]
}
# Allow state locking in DynamoDB
statement {
effect = "Allow"
actions = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
resources = ["arn:aws:dynamodb:*:*:table/terraform-state-locks"]
}
# Allow basic resource provisioning (EC2, VPC, ECS)
statement {
effect = "Allow"
actions = [
"ec2:*",
"ecs:*",
"rds:*",
"iam:PassRole",
"kms:Decrypt",
"kms:GenerateDataKey"
]
resources = ["*"] # Adjust to specific ARNs in a real enterprise context
}
}
# Create IAM Policy
resource "aws_iam_policy" "terraform_policy" {
name = "github-terraform-permissions-${var.environment}"
description = "Grants GitHub Actions permission to manage infrastructure"
policy = data.aws_iam_policy_document.terraform_permissions.json
}
# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "terraform_attach" {
role = aws_iam_role.terraform_execution.name
policy_arn = aws_iam_policy.terraform_policy.arn
}
output "role_arn" {
value = aws_iam_role.terraform_execution.arn
description = "The ARN of the role for GitHub to assume"
}
Step 4: GitHub Actions Integration
In your GitHub pipeline file .github/workflows/terraform.yml, you can now request passwordless temporary credentials. Simply add permissions to your job and use the official AWS action:
jobs:
plan:
runs-on: ubuntu-latest
permissions:
id-token: write # Mandatory for exchanging OIDC JSON Web Tokens (JWT)
contents: read # Required to checkout code
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-terraform-execution-role-dev # Output role ARN
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Terraform Init
run: terraform init
AWS exchanges the cryptographic OIDC identity token sent by GitHub for a 1-hour temporary STS Session token. No passwords, no access keys, zero permanent credentials in Git, and maximum security compliance!
Next Steps
Now that our network is secure and our authentication is passwordless, we move on to Module 3: Infrastructure Modularization & Scaling. In the next lesson, we'll learn how to write elite, reusable Terraform modules that enforce operational best practices automatically.