Configure GitHub Actions OIDC to AWS with Terraform State Lock and Audit Logging

Modern CI/CD infrastructure demands credential-less authentication between GitHub Actions and AWS. OpenID Connect (OIDC) federation eliminates long-lived secrets, while Terraform state locking prevents concurrent execution collisions. This guide assembles the documented configuration path for wiring these components together, with version-anchored steps and audit logging via CloudTrail.


Who This Setup Is Wrong For

Skip this architecture if you run single-developer Terraform workflows from a local machine with no CI pipeline, or if your organization mandates IAM user access keys with rotation policies that satisfy existing compliance frameworks. Teams using Terraform Enterprise with built-in remote state and locking already have equivalent controls managed by the platform. Organizations on Terraform versions prior to 1.10 who cannot upgrade should evaluate whether deprecated DynamoDB locking meets their support timeline.


Core Components and Version Anchors

Component Minimum Documented Version Source Basis
Terraform CLI 1.10.x HashiCorp release documentation
AWS Provider 5.x AWS IAM OIDC provider documentation
GitHub Actions Current aws-actions/configure-aws-credentials GitHub Actions documentation

The vendor documentation states that Terraform 1.10 introduced S3 native state locking as an opt-in experimental feature, with DynamoDB-based locking deprecated and scheduled for removal in a future minor version. The HashiCorp Developer documentation notes that the S3 backend now supports locking via S3 or DynamoDB, but explicitly warns that DynamoDB-based locking is deprecated.


Step 1: Bootstrap the OIDC Identity Provider in AWS

AWS IAM supports OIDC identity providers for federation. The vendor documentation establishes that after creating an IAM OIDC identity provider, you must create one or more IAM roles, as a role is an identity in AWS that does not have its own credentials.

Back Up First: Export Existing IAM Configuration

Before creating the OIDC provider, back up existing IAM roles and policies that might intersect with CI/CD permissions:

aws iam list-roles –query ‘Roles[].RoleName’ > iam-roles-backup-$(date +%Y%m%d).json
aws iam list-policies –scope Local –query ‘Policies[
].Arn’ > iam-policies-backup-$(date +%Y%m%d).json

This preserves rollback capability if provider or role creation produces unexpected policy attachment behavior.

Create the OIDC Provider

terraform/oidc-provider.tf

Terraform AWS Provider 5.x

resource “aws_iam_openid_connect_provider” “github” {
url = “https://token.actions.githubusercontent.com”

client_id_list = [
“sts.amazonaws.com”
]

thumbprint_list = [
“6938fd4e98bab03faadb97b34396831e3780aea1”,
“1c58a3a8518e8759bf075b76b750d4f2df264fcd”
]
}

The thumbprint values are documented by GitHub for the token.actions.githubusercontent.com issuer. AWS documentation states that the thumbprint is used to verify the certificate of the OIDC identity provider.


Step 2: Configure the IAM Trust Policy for GitHub Actions

The IAM role trust policy defines which GitHub repositories and references can assume the role. The vendor documentation establishes that you must create IAM roles after the OIDC identity provider exists.

terraform/iam-role.tf

data “aws_iam_policy_document” “github_trust” {
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   = ["repo:your-org/*:*"]
}

}
}

resource “aws_iam_role” “github_actions” {
name = “GitHubActionsTerraformRole”
assume_role_policy = data.aws_iam_policy_document.github_trust.json
}

Security applicability boundary: This trust policy uses StringLike with a wildcard repository pattern. Narrow this to specific repositories and, where possible, specific Git references (branches or tags) to reduce blast radius. The vendor documentation does not prescribe a single correct pattern—scope according to your organization’s repository structure.

This section describes security-relevant configuration. It is not security advice. Evaluate against your organization’s threat model and compliance requirements.


Step 3: Configure Terraform Backend with S3 Native State Locking

Terraform 1.10.x documentation introduces S3 native state locking as the preferred path, with DynamoDB locking deprecated. The HashiCorp Developer documentation states that locking can be enabled via S3 or DynamoDB, but DynamoDB-based locking is deprecated and will be removed in a future minor version.

Back Up First: Migrate Existing State Safely

If you have existing Terraform state in S3 with DynamoDB locking:

Pull current state locally as backup

terraform state pull > terraform-state-backup-$(date +%Y%m%d).tfstate

Verify state file integrity

terraform show -json terraform-state-backup-*.tfstate > /dev/null && echo “Valid”

S3 Native Locking Configuration (Terraform 1.10+)

terraform/backend.tf

terraform {
required_version = “>= 1.10.0”

backend “s3” {
bucket = “your-terraform-state-bucket”
key = “infrastructure/terraform.tfstate”
region = “us-east-1”
encrypt = true

# S3 native locking — Terraform 1.10 experimental
use_lockfile = true

}
}

The use_lockfile = true parameter enables the S3 native locking mechanism documented in Terraform 1.10 release materials. This eliminates the DynamoDB table dependency that previous configurations required.

Legacy DynamoDB Locking (Deprecated Path)

For teams unable to adopt Terraform 1.10 immediately, the documented fallback:

terraform {
backend “s3” {
bucket = “your-terraform-state-bucket”
key = “infrastructure/terraform.tfstate”
region = “us-east-1”
encrypt = true
dynamodb_table = “terraform-state-lock”
}
}

The vendor documentation states this path is deprecated. Plan migration to S3 native locking before the removal in a future minor version.


Step 4: GitHub Actions Workflow Configuration

The GitHub Actions documentation establishes that anyone with write access to a repository can run actions, and any costs of running the actions are billed to the repository owner. Minutes are free for public repositories.

.github/workflows/terraform.yml

GitHub Actions workflow — aws-actions/configure-aws-credentials@v4

name: Terraform CI/CD

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
id-token: write
contents: read

jobs:
terraform:
runs-on: ubuntu-latest
steps:
– name: Checkout
uses: actions/checkout@v4

  - name: Configure AWS Credentials via OIDC
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::YOUR_ACCOUNT_ID:role/GitHubActionsTerraformRole
      aws-region: us-east-1

  - name: Setup Terraform
    uses: hashicorp/setup-terraform@v3
    with:
      terraform_version: "1.10.0"

  - name: Terraform Init
    run: terraform init

  - name: Terraform Plan
    run: terraform plan -out=tfplan

  - name: Terraform Apply
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: terraform apply -auto-approve tfplan

The workflow uses OIDC token exchange to obtain temporary AWS credentials. No access keys are stored in GitHub secrets.


Step 5: Enable CloudTrail Audit Logging for Terraform Operations

AWS CloudTrail captures API calls made by the assumed IAM role. The vendor documentation establishes that IAM roles are identities in AWS that do not have their own credentials—CloudTrail logs the role assumption and subsequent API activity.

CloudTrail Configuration for Terraform Audit Trail

terraform/cloudtrail.tf

resource “aws_cloudtrail” “terraform_audit” {
name = “terraform-audit-trail”
s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id
include_global_service_events = true
is_multi_region_trail = true
enable_logging = true

event_selector {
read_write_type = “All”
include_management_events = true
exclude_management_event_sources = []
}
}

resource “aws_s3_bucket” “cloudtrail_logs” {
bucket = “terraform-audit-logs-${data.aws_caller_identity.current.account_id}”
}

resource “aws_s3_bucket_policy” “cloudtrail_logs” {
bucket = aws_s3_bucket.cloudtrail_logs.id
policy = data.aws_iam_policy_document.cloudtrail_s3.json
}

Security applicability boundary: CloudTrail captures management events by default. Data events (S3 object-level operations) require explicit configuration and incur additional charges. The vendor documentation does not specify exact pricing for data events—check the current AWS CloudTrail pricing page for your region and usage volume.

This section describes security-relevant configuration. It is not security advice. Logging scope should align with your organization’s compliance and incident response requirements.


Cost Considerations from Documented Sources

Service Pricing Basis Documented Figure
GitHub Actions Per-minute runner usage Public repositories: free minutes; lower runner rates available following re-architecture (vendor pricing update documentation)
HCP Terraform Per managed resource, hourly Essentials plan: per-hour rate varies by managed resource count (vendor documentation example calculation)
AWS IAM OIDC No standalone charge Included with IAM (vendor documentation)
AWS CloudTrail Management events: first trail free per region; data events and additional trails vary Check vendor pricing page for current rates

The HashiCorp Developer documentation provides an example calculation for HCP Terraform Essentials: a given managed resource count operating continuously for a full month accrues an hourly cost based on the per-resource rate. This is illustrative—actual costs depend on resource count and operational hours.


Verification Checklist

Check Verification Method
OIDC provider thumbprint valid aws iam get-open-id-connect-provider --open-id-connect-provider-arn <arn>
Role trust policy restricts expected repositories aws iam get-role --role-name GitHubActionsTerraformRole
State locking active Run concurrent terraform plan operations; second should block
CloudTrail logging active aws cloudtrail get-trail-status --name terraform-audit-trail
No long-lived credentials in GitHub secrets Repository Settings → Secrets and variables → Actions

Summary of Version-Anchored Decisions

Terraform 1.10.x introduces S3 native state locking as the forward path, with DynamoDB locking deprecated. GitHub Actions OIDC to AWS requires Terraform AWS Provider 5.x for current IAM resource support. The aws-actions/configure-aws-credentials action at v4 supports the OIDC flow documented here.

Organizations on older Terraform versions must weigh the deprecation timeline against upgrade capacity. The vendor documentation states DynamoDB locking will be removed in a future minor version—no specific version is announced.

All security-relevant configuration in this guide is provided for technical accuracy against vendor documentation. It is not security advice. Evaluate applicability against your organization’s specific requirements, threat model, and compliance obligations.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *