Lesson 6 of 15 5 minAdvanced Track

IAM Least Privilege: Roles, Policies, and OIDC

Master IAM roles, policies, least privilege mechanics, and set up passwordless OIDC authentication for GitHub Actions pipelines.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Key Takeaways

  • Never store static AWS Access Keys (AWS_ACCESS_KEY_ID) inside GitHub Secrets.
  • IAM Policies grant permissions (actions/resources), while Trust Policies define WHO can assume the role.
  • OpenID Connect (OIDC) establishes trust between GitHub and AWS, exchanging temporary OAuth tokens for execution sessions.
Recommended Prerequisites
terraform-aws-05-nat-gateway-security-groups

Premium outcome

Provision, secure, and automate production-grade cloud infrastructure at scale.

Backend and platform engineers who want to design, deploy, and automate robust production environments on AWS.

You leave with

  • A secure, modular, multi-environment AWS landing zone designed from scratch
  • A fully integrated GitOps deployment pipeline using GitHub Actions and Terraform S3 Backend
  • Hands-on expertise deploying containerized microservices (ECS Fargate + RDS) with secure IAM gating

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:

  1. Adhere strictly to the Principle of Least Privilege.
  2. 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:

  1. IAM Policy: A JSON document that defines what actions are allowed or denied on what resources.
  2. 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.
  3. 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.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.