Questions
Workflows use long-lived credentials that could be leaked. Implement secure authentication with OIDC.
The Scenario
Security audit findings on your GitHub Actions workflows:
Critical Issues Found:
1. AWS access keys stored as repository secrets (90+ day old)
2. Same credentials used across dev/staging/prod
3. No credential rotation policy
4. Keys have admin-level permissions
5. Secrets exposed in fork PRs from external contributors
A leaked credential could give attackers access to your entire cloud infrastructure.
The Challenge
Implement secure, short-lived authentication using OpenID Connect (OIDC) that eliminates long-lived credentials and provides fine-grained access control.
A junior engineer might just rotate the secrets more frequently, restrict which workflows can use secrets, or add more secrets for different environments. These approaches still rely on long-lived credentials that can be leaked, exfiltrated, or misused.
A senior engineer implements OIDC federation which allows GitHub Actions to authenticate directly with cloud providers using short-lived tokens. No secrets to leak, automatic rotation, and fine-grained access control based on repository, branch, and environment.
Step 1: Understand OIDC Flow
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GitHub Actions │────▶│ GitHub OIDC │────▶│ AWS STS │
│ Workflow │ │ Provider │ │ AssumeRole │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ 1. Request token │ │
│──────────────────────▶│ │
│ │ │
│ 2. JWT with claims │ │
│◀──────────────────────│ │
│ │ │
│ 3. Present JWT to assume role │
│──────────────────────────────────────────────▶│
│ │ │
│ 4. Short-lived credentials │
│◀──────────────────────────────────────────────│Step 2: Configure AWS OIDC Provider (Terraform)
# oidc.tf - Create OIDC provider in AWS
# GitHub's OIDC provider
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
# IAM role for GitHub Actions - Production deployments
resource "aws_iam_role" "github_actions_deploy_prod" {
name = "github-actions-deploy-prod"
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 = {
# Only allow from specific repo AND main branch AND production environment
"token.actions.githubusercontent.com:sub" = "repo:my-org/my-app:environment:production"
}
}
}
]
})
# Least privilege - only what's needed for deployment
inline_policy {
name = "deploy-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"eks:DescribeCluster",
"eks:ListClusters"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
Resource = "*"
}
]
})
}
}
# Role for staging - more permissive conditions
resource "aws_iam_role" "github_actions_deploy_staging" {
name = "github-actions-deploy-staging"
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 = {
# Allow from main branch or staging environment
"token.actions.githubusercontent.com:sub" = [
"repo:my-org/my-app:ref:refs/heads/main",
"repo:my-org/my-app:environment:staging"
]
}
}
}
]
})
}
# Output role ARNs for workflow configuration
output "prod_role_arn" {
value = aws_iam_role.github_actions_deploy_prod.arn
}
output "staging_role_arn" {
value = aws_iam_role.github_actions_deploy_staging.arn
}Step 3: Configure GitHub Workflow with OIDC
name: Deploy with OIDC
on:
push:
branches: [main]
# Required for OIDC token
permissions:
id-token: write # Required for requesting JWT
contents: read # Required for actions/checkout
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # Links to GitHub environment
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-staging
aws-region: us-east-1
# Optional: customize session
role-session-name: GitHubActions-${{ github.run_id }}
role-duration-seconds: 900 # 15 minutes max
- name: Verify credentials
run: |
aws sts get-caller-identity
# Shows: arn:aws:sts::123456789012:assumed-role/github-actions-deploy-staging/GitHubActions-xxx
- name: Deploy to staging
run: |
aws eks update-kubeconfig --name staging-cluster
kubectl apply -f k8s/staging/
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires approval if configured
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
# Different role for production - more restrictive
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-prod
aws-region: us-east-1
- name: Deploy to production
run: |
aws eks update-kubeconfig --name production-cluster
kubectl apply -f k8s/production/Step 4: Configure GCP Workload Identity Federation
# For Google Cloud
name: Deploy to GCP
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'github-actions@project-id.iam.gserviceaccount.com'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Deploy
run: |
gcloud container clusters get-credentials cluster-name --region us-central1
kubectl apply -f k8s/Step 5: Configure Azure OIDC
# For Azure
name: Deploy to Azure
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to AKS
run: |
az aks get-credentials --resource-group myRG --name myAKS
kubectl apply -f k8s/Step 6: Environment Protection Rules
# Repository Settings > Environments > production
# Configure in GitHub UI:
# 1. Required reviewers: platform-team
# 2. Wait timer: 5 minutes
# 3. Deployment branches: main only
# 4. Environment secrets (if any)
# Workflow using protected environment
name: Production Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.company.com
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
# This step will wait for approval before running
- name: Configure AWS (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }} # Environment variable
aws-region: us-east-1
- name: Deploy
run: ./deploy.shStep 7: Security Best Practices
name: Secure Workflow
on:
push:
branches: [main]
pull_request:
# Minimum required permissions
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
# Only deploy job needs elevated permissions
deploy:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
# Job-level permissions (overrides workflow-level)
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-east-1
# Never echo secrets or tokens
- name: Deploy (without exposing credentials)
run: |
# AWS credentials are automatically available as env vars
# Don't: echo $AWS_ACCESS_KEY_ID
# Don't: printenv | grep AWS
aws eks update-kubeconfig --name prod-cluster
kubectl apply -f k8s/ OIDC Claim Conditions
| Claim | Example Value | Use Case |
|---|---|---|
sub | repo:org/repo:ref:refs/heads/main | Restrict to specific branch |
sub | repo:org/repo:environment:production | Restrict to environment |
sub | repo:org/repo:pull_request | Allow/deny PR workflows |
repository | org/repo | Restrict to specific repo |
repository_owner | org | Allow any repo in org |
Practice Question
What is the main security advantage of using OIDC for GitHub Actions authentication over storing cloud credentials as secrets?