How I eliminated access keys from my deployment pipeline with OIDC, Terraform, and GitHub Actions

Source: Dev.to | Jan 23, 2026 (UTC)

The Problem with Traditional CI/CD

Most tutorials for deploying static sites to AWS tell you to:

  1. Generate AWS access keys
  2. Store them as GitHub secrets
  3. Hope nobody compromises your repository

This approach has serious security implications. Long-lived credentials in CI/CD pipelines are a common attack vector since if your repository is compromised or secrets are accidentally exposed, attackers gain full AWS access with those credentials.

There’s a better way: OpenID Connect (OIDC) federation.

What I Built

A fully automated static website deployment pipeline that eliminates access keys entirely by using temporary credentials exchanged through OIDC.

Live Demo: https://d2jgqhup9totr6.cloudfront.net

Source Code: GitHub Repository

Architecture Overview

Developer commits → GitHub Actions → OIDC token exchange → Temporary AWS credentials → Deploy to S3 → CloudFront invalidation

Enter fullscreen mode

Exit fullscreen mode

Key Components:

Why OIDC Over Access Keys?

Traditional Approach (Access Keys):

# Potential security risk - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Enter fullscreen mode

Exit fullscreen mode

Problems:

OIDC Approach:

# This is secure - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ${{ secrets.AWS_REGION }}

Enter fullscreen mode

Exit fullscreen mode

Benefits:

Implementation Deep Dive

1. Setting Up the OIDC Provider in Terraform

First, create an OIDC identity provider that trusts GitHub:

resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] }

Enter fullscreen mode

Exit fullscreen mode

The thumbprint verifies GitHub’s signing certificate authenticity.

2. Creating the IAM Role with Trust Policy

For this to work, we define exactly which repositories can assume this role:

resource "aws_iam_role" "github_actions" { name = "github-actions-s3-deployment" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.github.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" } StringLike = { "token.actions.githubusercontent.com:sub" = "repo:${var.github_username}/${var.repo_name}:ref:refs/heads/main" } } }] }) }

Enter fullscreen mode

Exit fullscreen mode

Critical security feature: The StringLike condition restricts access to:

Even if someone forks your repo, they can’t assume this role.

3. Least-Privilege IAM Permissions

The role only gets permissions it absolutely needs:

resource "aws_iam_role_policy" "github_actions" { role = aws_iam_role.github_actions.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket" ] Resource = [ aws_s3_bucket.website_storage.arn, "${aws_s3_bucket.website_storage.arn}/*" ] }, { Effect = "Allow" Action = "cloudfront:CreateInvalidation" Resource = aws_cloudfront_distribution.website_storage.arn } ] }) }

Enter fullscreen mode

Exit fullscreen mode

No overly broad permissions.

4. CloudFront Configuration Challenge

Here’s something that tripped me up: S3 website endpoints require custom origin configuration, not standard S3 origin configuration.

# This doesn't work for S3 website endpoints origin { domain_name = aws_s3_bucket.website_storage.bucket_regional_domain_name origin_id = "S3-Website" s3_origin_config { origin_access_identity = aws_cloudfront_origin_access_identity.default.cloudfront_access_identity_path } } # This works for the s3 website endpoints origin { domain_name = aws_s3_bucket.website_storage.website_endpoint origin_id = "S3-Website" custom_origin_config { http_port = 80 https_port = 443 origin_protocol_policy = "http-only" origin_ssl_protocols = ["TLSv1.2"] } }

Enter fullscreen mode

Exit fullscreen mode

Why? S3 website endpoints function as HTTP servers, supporting redirects and error documents. Standard S3 origins access the S3 API directly and don’t support these features.

5. Remote State Management

For production infrastructure, always use remote state with locking:

terraform { backend "s3" { bucket = "escanut-tf-state" key = "s3-website/terraform.tfstate" region = "us-east-1" dynamodb_table = "tf-state-lock" encrypt = true } }

Enter fullscreen mode

Exit fullscreen mode

This will enable:

The GitHub Actions Workflow

The deployment workflow is remarkably simple:

name: Deploy to S3 on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write # Required for OIDC contents: read steps: - 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: ${{ secrets.AWS_REGION }} - name: Sync to S3 run: | aws s3 sync ./build s3://${{ secrets.S3_BUCKET }} --delete - name: Invalidate CloudFront run: | aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CLOUDFRONT_ID }} \ --paths "/*" - name: Deployment Overview run: | echo "Website deployment successful" echo "https://${{ secrets.CLOUDFRONT_URL }}"

Enter fullscreen mode

Exit fullscreen mode

Deployment time: Approximately 30 seconds from commit to live site (measured from workflow logs).

Cost Analysis

One of the best parts? This infrastructure costs almost nothing to run.

Monthly costs (based on January 2026 AWS pricing):

Service

Usage

Cost

S3 Storage

<1GB

$0.02

S3 Requests

10k/month

$0.05

CloudFront

10GB transfer

$0.85

Total

~$0.92/month

For comparison, a single t3.micro EC2 instance costs $8-15/month and requires active management.
.

AWS’s eventually consistent nature means public access settings must be applied before the bucket policy.

Security Wins

This architecture eliminates several attack vectors:

No long-lived credentials – temporary credentials expire in 1 hour

Repository-scoped access – only your specific repo can deploy

Branch-specific trust – only main branch can trigger deployments

Least-privilege permissions – role can only write to specific S3 bucket

Encrypted state – Terraform state encrypted at rest in S3

No credential rotation – AWS handles credential lifecycle automatically

The current implementation shows:

Conclusion

OIDC federation with GitHub Actions is the modern standard for CI/CD authentication to AWS. It’s more secure, requires zero maintenance, and is easier to implement than managing long-lived access keys.

If you’re still using AWS access keys in your deployment pipelines, it’s time to upgrade.

Try it yourself: Fork the repository, update the variables, and deploy your own infrastructure in under 5 minutes.

Drop a comment below or reach out on LinkedIn.

If this was helpful, consider giving the GitHub repo a ⭐!

For further actions, you may consider blocking this person and/or reporting abuse

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Читайте також