Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. How to Secure GitHub Actions Workflows with OIDC, SHA
How to Secure GitHub Actions Workflows with OIDC, SHA
HOWTOIntermediate

How to Secure GitHub Actions Workflows with OIDC, SHA

Harden your CI/CD pipeline by replacing long-lived secrets with OIDC short-lived tokens, pinning third-party actions to commit SHAs, enforcing...

Dylan H.

Security Engineering

March 9, 2026
13 min read

Prerequisites

  • GitHub repository with Actions enabled
  • Cloud account — Azure, AWS, or GCP (examples cover Azure and AWS)
  • Repository admin or Organization owner permissions to configure OIDC and environment protection rules
  • Familiarity with YAML workflow syntax and basic cloud IAM concepts

Overview

Every GitHub Actions workflow that deploys to cloud infrastructure is a potential supply chain attack vector. Long-lived credentials stored as repository secrets are high-value targets — a single leaked secret can grant an attacker persistent cloud access. Third-party actions pulled from the Marketplace introduce transitive dependencies that can be silently compromised if pinned only to a mutable tag.

This guide covers the full hardening stack:

What You Will Learn:

  • Replace static cloud credentials with OIDC short-lived tokens for Azure and AWS
  • Pin all third-party actions to immutable commit SHAs
  • Scope workflow permissions to the minimum required
  • Configure environment protection rules to gate production deployments
  • Use OpenSSF Scorecard and StepSecurity Harden-Runner for continuous supply chain visibility

Who Should Use This Guide:

  • DevOps and platform engineers managing GitHub Actions pipelines
  • Security engineers auditing CI/CD for supply chain risk
  • Cloud architects migrating from long-lived service principal credentials to keyless auth

How OIDC Keyless Authentication Works

Traditional CI/CD pipelines store a cloud credential (Azure service principal secret, AWS access key) as a GitHub secret. That secret lives indefinitely, can be exfiltrated from workflow logs, and must be rotated manually.

With OIDC, the token exchange works like this:

GitHub Actions Job starts
        │
        ▼
GitHub generates a signed OIDC JWT
(contains: repo, workflow, ref, environment, job claims)
        │
        ▼
Workflow requests cloud token from provider
(Azure AD / AWS STS) using the JWT
        │
        ▼
Cloud provider validates JWT against GitHub's OIDC issuer
(https://token.actions.githubusercontent.com)
        │
        ▼
Cloud provider issues a short-lived access token
(valid only for the current job — expires when job ends)
        │
        ▼
Workflow uses token to deploy/access resources

Key properties of OIDC tokens:

PropertyValue
LifetimeSingle job only — auto-expires
StorageNever stored as a secret — generated per-run
RotationAutomatic — no manual rotation needed
ScopeConstrained by cloud IAM trust policy
AuditabilityFull claim set visible in cloud provider logs

Section 1: Configure OIDC for Azure

1.1 Create a Federated Identity Credential on the App Registration

You need an Entra ID App Registration (or Managed Identity) with a federated credential that trusts your GitHub repository.

Step 1 — Create the App Registration (if not already present)

az ad app create --display-name "github-actions-oidc-myrepo"
APP_ID=$(az ad app list --display-name "github-actions-oidc-myrepo" --query "[0].appId" -o tsv)
 
# Create service principal for the app
az ad sp create --id "$APP_ID"
SP_OBJECT_ID=$(az ad sp show --id "$APP_ID" --query "id" -o tsv)

Step 2 — Assign the required RBAC role

Grant only the permissions the workflow needs — e.g., Contributor on a specific resource group, not the subscription:

RESOURCE_GROUP="rg-production"
SUBSCRIPTION_ID=$(az account show --query "id" -o tsv)
 
az role assignment create \
  --assignee "$SP_OBJECT_ID" \
  --role "Contributor" \
  --scope "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}"

Step 3 — Add the federated credential

az ad app federated-credential create \
  --id "$APP_ID" \
  --parameters '{
    "name": "github-prod-branch",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:MyOrg/MyRepo:environment:production",
    "description": "GitHub Actions production environment",
    "audiences": ["api://AzureADTokenExchange"]
  }'

Subject claim patterns:

Deploy triggerSubject claim
Specific environmentrepo:MyOrg/MyRepo:environment:production
Specific branchrepo:MyOrg/MyRepo:ref:refs/heads/main
Pull requestsrepo:MyOrg/MyRepo:pull_request
Specific tagrepo:MyOrg/MyRepo:ref:refs/tags/v*

Principle of least privilege: Create separate federated credentials (and separate App Registrations) for staging and production. Never share a single credential across environments.

1.2 Store App Registration Details as GitHub Secrets (one-time setup)

In your repository Settings → Secrets and variables → Actions, add:

Secret nameValue
AZURE_CLIENT_IDApp Registration Application (client) ID
AZURE_TENANT_IDEntra ID Tenant ID
AZURE_SUBSCRIPTION_IDAzure Subscription ID

These are non-sensitive identifiers — no password or secret value is stored.

1.3 Update the Workflow to Use OIDC

name: Deploy to Azure (OIDC)
 
on:
  push:
    branches: [main]
 
# Minimum permissions — OIDC requires id-token: write
permissions:
  id-token: write
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # gates on environment protection rules
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Azure Login (OIDC — no secrets)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy to Azure App Service
        run: |
          az webapp deploy \
            --resource-group rg-production \
            --name my-app \
            --src-path ./dist/app.zip

Section 2: Configure OIDC for AWS

2.1 Create the OIDC Identity Provider in AWS

# Add GitHub Actions as a trusted OIDC provider
aws iam create-open-id-connect-provider \
  --url "https://token.actions.githubusercontent.com" \
  --client-id-list "sts.amazonaws.com" \
  --thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1"

2.2 Create an IAM Role with a Trust Policy

Create trust-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:MyOrg/MyRepo:environment:production"
        }
      }
    }
  ]
}
# Create the role
aws iam create-role \
  --role-name GitHubActions-MyRepo-Production \
  --assume-role-policy-document file://trust-policy.json
 
# Attach only the permissions the workflow needs
aws iam attach-role-policy \
  --role-name GitHubActions-MyRepo-Production \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess  # scope this further in production

2.3 AWS OIDC Workflow

name: Deploy to AWS S3 (OIDC)
 
on:
  push:
    branches: [main]
 
permissions:
  id-token: write
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
 
    steps:
      - name: Checkout
        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/GitHubActions-MyRepo-Production
          aws-region: us-east-1
 
      - name: Sync to S3
        run: aws s3 sync ./dist s3://my-production-bucket --delete

Section 3: Pin Actions to Commit SHAs

Tags like @v4 are mutable — an action author (or attacker who compromises the author's account) can move the tag to a different commit. Pinning to the full SHA is the only guarantee of immutability.

3.1 Find the SHA for an Action

# Using GitHub CLI — find the SHA for the commit a tag points to
gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha'
# For annotated tags, follow the tag object to the commit:
gh api repos/actions/checkout/git/tags/$(gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha') --jq '.object.sha'

Or simply navigate to the action's GitHub repository, click the tag, and copy the full 40-character commit SHA from the URL.

3.2 Pinned Workflow Example

steps:
  # ✅ Pinned to SHA — immutable
  - name: Checkout
    uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
 
  - name: Setup Node.js
    uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0
 
  - name: Azure Login
    uses: azure/login@a65d910e8af852a8061c627c456678983e180302  # v2.2.0
 
  # ❌ Avoid — tag is mutable
  # uses: actions/checkout@v4

3.3 Automate SHA Updates with Dependabot

Add .github/dependabot.yml to keep pinned SHAs current:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"

Dependabot will open PRs that update the SHA comment and the pinned hash whenever a new version is released.


Section 4: Minimum Workflow Permissions

By default, GitHub grants read on all scopes and write on contents. Override at the workflow and job level to enforce least privilege.

4.1 Default-Deny at the Workflow Level

# Deny all permissions by default for the entire workflow
permissions: {}
 
jobs:
  build:
    runs-on: ubuntu-latest
    # Grant only what this job needs
    permissions:
      contents: read
      packages: write   # only if pushing to GHCR
 
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC token exchange
      contents: read

4.2 Permissions Reference

PermissionWhen to grant
contents: readMost jobs — checkout code
contents: writeCommit back to the repo (changelogs, release notes)
id-token: writeOIDC cloud authentication — required
packages: writePush to GitHub Container Registry
pull-requests: writeComment on PRs, approve
issues: writeOpen or comment on issues
security-events: writeUpload SARIF results to Code Scanning

Section 5: Environment Protection Rules

GitHub Environments add a gating layer between your workflow and production deployment. Combined with OIDC subject claims scoped to the environment, this creates a two-layer control.

5.1 Configure the Production Environment

  1. Navigate to Repository → Settings → Environments → New environment
  2. Name it production
  3. Under Deployment protection rules, configure:
RuleRecommended setting
Required reviewersAdd your on-call team (1-2 approvers)
Wait timer0–5 minutes (gives time to cancel errant runs)
Deployment branchesSelected branches: main only
Prevent self-reviewEnabled — author cannot approve their own PR

5.2 Environment Secrets vs Repository Secrets

Store production credentials in the Environment secret store, not the repository secret store. Environment secrets are only injected into jobs that reference environment: production and have passed the protection rules.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # protection rules enforced here
    steps:
      - run: echo "This step has access to production environment secrets"

Section 6: Supply Chain Scanning

6.1 OpenSSF Scorecard

Scorecard runs automated checks against your repository for common supply chain risks.

# .github/workflows/scorecard.yml
name: Scorecard Supply Chain Security
 
on:
  branch_protection_rule:
  schedule:
    - cron: "30 1 * * 0"   # weekly Sunday 01:30 UTC
  push:
    branches: [main]
 
permissions:
  security-events: write
  id-token: write
  contents: read
 
jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          persist-credentials: false
 
      - name: Run analysis
        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46  # v2.4.0
        with:
          results_file: results.sarif
          results_format: sarif
          publish_results: true
 
      - name: Upload SARIF results to Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

Key Scorecard checks:

CheckWhat it verifies
Pinned-DependenciesActions pinned to SHA, not tags
Token-PermissionsWorkflow permissions minimized
Branch-ProtectionMain branch requires PR reviews
SASTStatic analysis running in CI
Secret-ScanningGitHub secret scanning enabled
Dependency-Update-ToolDependabot or Renovate configured
VulnerabilitiesNo unfixed OSV vulnerabilities in dependencies

6.2 StepSecurity Harden-Runner

Harden-Runner monitors network egress and syscalls from your workflow runners, detecting unexpected outbound connections that could indicate a compromised action.

steps:
  - name: Harden Runner
    uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf  # v2.11.1
    with:
      egress-policy: audit   # use 'block' once you've established the baseline
 
  - name: Checkout
    uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

After a few runs, switch egress-policy to block with an allowed-endpoints list:

    with:
      egress-policy: block
      allowed-endpoints: >
        github.com:443
        api.github.com:443
        registry.npmjs.org:443
        objects.githubusercontent.com:443

Any unexpected outbound connection will fail the job and appear in the workflow summary.


Section 7: Audit and Rotate Legacy Secrets

If you are migrating from long-lived credentials to OIDC, follow this rotation sequence to avoid downtime:

[ ] Inventory all repository and organization secrets
    Settings → Secrets and variables → Actions

[ ] For each cloud credential secret:
    [ ] Identify which workflows consume it
    [ ] Configure OIDC federated credential for the cloud resource
    [ ] Update workflow to use OIDC login step
    [ ] Test in a non-production branch first
    [ ] Merge and validate a successful production run
    [ ] Delete the legacy secret from GitHub

[ ] For any remaining non-OIDC secrets (API keys, database passwords):
    [ ] Verify they are scoped to the minimum required permissions
    [ ] Move production secrets to Environment secret store (not repo-level)
    [ ] Confirm rotation schedule exists (90 days maximum)
    [ ] Enable GitHub secret scanning push protection:
        Settings → Code security → Secret scanning → Push protection

[ ] Enable branch protection on main:
    [ ] Require pull request reviews (1 reviewer minimum)
    [ ] Require status checks to pass (CI build, tests)
    [ ] Require branches to be up to date
    [ ] Restrict who can push directly to main

Hardened Workflow Template

The following is a complete, production-hardened deployment workflow combining all controls from this guide:

name: Production Deploy
 
on:
  push:
    branches: [main]
 
# Default-deny all permissions
permissions: {}
 
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
 
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf  # v2.11.1
        with:
          egress-policy: block
          allowed-endpoints: >
            github.com:443
            registry.npmjs.org:443
 
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
 
      - name: Setup Node.js
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0
        with:
          node-version: "20"
          cache: "npm"
 
      - name: Install and build
        run: |
          npm ci
          npm run build
 
      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
 
  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment: production   # enforces reviewer approval + branch protection
    permissions:
      id-token: write   # OIDC token exchange
      contents: read
 
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf  # v2.11.1
        with:
          egress-policy: block
          allowed-endpoints: >
            github.com:443
            management.azure.com:443
            login.microsoftonline.com:443
 
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
 
      - name: Azure Login (OIDC — no long-lived secret)
        uses: azure/login@a65d910e8af852a8061c627c456678983e180302  # v2.2.0
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy
        run: |
          az webapp deploy \
            --resource-group rg-production \
            --name my-app \
            --src-path ./dist/app.zip \
            --type zip

Troubleshooting

OIDC token exchange fails: Error: The client with id ... does not have permission

  1. Verify the subject claim in the federated credential matches exactly what GitHub sends. Check the job's OIDC token claims:
    - name: Debug OIDC claims
      run: |
        TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
          "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange")
        echo $TOKEN | jq -r '.value' | cut -d. -f2 | base64 -d 2>/dev/null | jq .
  2. Confirm permissions: id-token: write is set at the job or workflow level
  3. Confirm the environment: name in the workflow matches the federated credential subject exactly (case-sensitive)

Actions checkout fails after pinning to SHA

Dependabot or a manual SHA update may have broken the checkout step if the SHA no longer exists. Verify the SHA against the repository:

gh api repos/actions/checkout/commits/THE_SHA --jq '.sha'

Scorecard fails Pinned-Dependencies even after pinning

Scorecard inspects the SHA comment on the same line. The format must be:

uses: actions/checkout@SHA  # vX.Y.Z

A SHA with no comment or a comment on the next line may not be recognized by older Scorecard versions.


Summary

ControlTool / MethodImpact
Keyless cloud authOIDC federated credentialsEliminates long-lived credential exposure
Immutable action versionsSHA pinning + DependabotPrevents supply chain via compromised action tag
Least privilegepermissions: {} + per-job grantsLimits blast radius of compromised workflow
Deployment gatingGitHub Environments + reviewersRequires human approval before production
Runtime monitoringStepSecurity Harden-RunnerDetects unexpected network egress
Continuous scoringOpenSSF ScorecardOngoing supply chain posture visibility
Secret hygienePush protection + environment secretsPrevents credential leakage at commit time

Implementing all controls reduces your CI/CD attack surface from a persistent credential that can be exfiltrated and reused indefinitely, to a short-lived, job-scoped token that is automatically revoked and cryptographically bound to a specific repository, branch, and environment.

Related Reading

  • CI/CD Pipeline with GitHub Actions and Azure
  • UNC6426 Weaponizes Old nx npm Supply Chain Compromise to
  • VoidLink: AI-Generated Cloud-Native Malware Framework
#GitHub Actions#OIDC#Supply Chain Security#CI/CD#DevSecOps#Secrets Management#Cloud Security#Azure#AWS

Related Articles

How to Configure Microsoft Sentinel Analytics Rules

End-to-end SOC guide for Microsoft Sentinel: build KQL-based scheduled and NRT analytics rules, wire automation rules for incident triage, and deploy...

15 min read

AWS Security Hub: Centralized Security Findings

Implement AWS Security Hub for centralized security findings across accounts. Covers security standards, GuardDuty/Inspector integration, custom insights,...

13 min read

HashiCorp Vault: Centralized Secrets Management for Modern Infrastructure

Deploy and configure HashiCorp Vault to securely store, rotate, and audit secrets across your infrastructure — covering installation, auth methods,...

8 min read
Back to all HOWTOs