DeployU
Interviews / DevOps & Cloud Infrastructure / Workflows use long-lived credentials that could be leaked. Implement secure authentication with OIDC.

Workflows use long-lived credentials that could be leaked. Implement secure authentication with OIDC.

practical Security & Secrets Interactive Quiz Code Examples

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.

Wrong Approach

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.

Right Approach

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.sh

Step 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

ClaimExample ValueUse Case
subrepo:org/repo:ref:refs/heads/mainRestrict to specific branch
subrepo:org/repo:environment:productionRestrict to environment
subrepo:org/repo:pull_requestAllow/deny PR workflows
repositoryorg/repoRestrict to specific repo
repository_ownerorgAllow 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?