Step 1

OIDC & Credential-Free Deployment

On this page

Exercise 1: OIDC & Credential-Free Deployment (~15 min)

Goal: Understand the supply chain attack that OIDC prevents, configure credential-free deployment with properly scoped trust, and apply defense-in-depth with action pinning.

πŸ“ Open docs/oidc-comparison-worksheet.md β€” record your BEFORE/AFTER observations as you work through this exercise.


Threat Framing: The Supply Chain Attack That OIDC Prevents

The Attack Chain:

Step 1: AZURE_CREDENTIALS leaked
        (log file exposure / compromised runner / insider / dependency attack)
    ↓
Step 2: Attacker authenticates as your service principal
        (the secret is long-lived β€” it doesn't expire for 1-2 years)
    ↓
Step 3: Attacker pushes malicious container image to your registry
        (they have full deployment permissions)
    ↓
Step 4: Malicious image deployed to your production cluster
        (indistinguishable from a legitimate deployment)
    ↓
Step 5: Production compromised β€” data exfiltrated, service disrupted
        (you may not know for days or weeks)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ OIDC breaks this chain at Step 1:                       β”‚
β”‚ There is NO long-lived credential to steal.             β”‚
β”‚ The OIDC token is short-lived (~10 min), scoped to a    β”‚
β”‚ specific workflow run, and tied to a specific            β”‚
β”‚ repository, branch, and commit SHA.                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Note: This is not hypothetical. In March 2025, the tj-actions/changed-files supply chain attack (CVE-2025-30066) demonstrated that compromised CI/CD components can exfiltrate secrets from thousands of repositories. OIDC eliminates the most valuable target β€” long-lived deployment credentials.


Step 1: Review the BEFORE Workflow

Open .github/workflows/build-deploy.yml and examine the authentication section:

    # ⚠️ INSECURE: Long-lived static credential
    - name: Azure Login
      uses: azure/login@v2
      with:
        creds: $

What’s wrong here:

  • AZURE_CREDENTIALS is a JSON blob containing a client ID, client secret, tenant ID, and subscription ID
  • The client secret is long-lived (default: 1-2 years)
  • Anyone with access to this secret can authenticate as this service principal
  • Rotation is manual and error-prone

Step 2: Configure Azure Federated Credential

Create a federated identity credential that trusts GitHub Actions OIDC tokens:

# Get your Azure AD Application (Service Principal) Object ID
APP_OBJECT_ID=$(az ad app list --display-name "sp-devsecops-ws3-deploy" --query "[0].id" -o tsv)

# Create federated credential for the main branch
az ad app federated-credential create \
  --id $APP_OBJECT_ID \
  --parameters '{
    "name": "github-actions-main",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:{owner}/{repo}:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"],
    "description": "GitHub Actions OIDC for main branch deployments"
  }'

Store the non-secret identifiers as repository variables (not secrets):

# These are NOT secrets β€” they are public identifiers
gh variable set AZURE_CLIENT_ID --body "<your-client-id>"
gh variable set AZURE_TENANT_ID --body "<your-tenant-id>"
gh variable set AZURE_SUBSCRIPTION_ID --body "<your-subscription-id>"

Step 2.5: Understand Subject Claim Scoping β€” The Most Important Security Decision

The subject field in the federated credential determines WHO can use this trust. This is the most critical security decision in OIDC configuration.

Look at the subject claim you just configured:

"subject": "repo:{owner}/{repo}:ref:refs/heads/main"

This means: ONLY the workflow running on the main branch of this specific repository can authenticate. But what if you configured it differently?

Subject Claim Scope Risk Who Can Deploy
repo:org/* πŸ”΄ DANGEROUS ANY repository in the organization
repo:org/repo:* 🟑 Risky Any branch, PR, or tag in this repo
repo:org/repo:ref:refs/heads/main 🟒 Scoped Only the main branch
repo:org/repo:environment:production 🟒 Best Only the production GitHub environment

Verify your configuration:

  1. Read back the subject claim you configured β€” is it scoped to main? βœ…
  2. Think: What would happen if you used repo:org/* (wildcard)? β†’ Any repo in the org could deploy to your cloud. A compromised experimental repo could push to production.
  3. Think: For a production deployment at your company, which scope would you use? Why?

⚠️ Wildcard subject claims are the OIDC equivalent of sharing a password. They defeat the purpose of federated identity. Always scope to the narrowest possible claim β€” specific repo + specific branch or environment.


Step 3: Review the AFTER Workflow

Open .github/workflows/build-deploy-oidc.yml and compare the authentication section:

permissions:
  id-token: write   # Required for OIDC token exchange
  contents: read

steps:
    # βœ… SECURE: Short-lived OIDC token, no stored secrets
    - name: Azure Login (OIDC)
      uses: azure/login@v2
      with:
        client-id: $
        tenant-id: $
        subscription-id: $

What changed:

  • No secrets.AZURE_CREDENTIALS β€” no long-lived secret anywhere
  • permissions: id-token: write β€” the workflow requests an OIDC token from GitHub
  • Azure validates the token against the federated credential β€” trusting the identity, not a secret
  • The token is short-lived (valid for ~10 minutes) and scoped to this specific workflow run

⚠️ Defense-in-Depth: Pin Actions to Commit SHAs

OIDC protects your credentials. But what if the azure/login action ITSELF is compromised? The 2025 supply chain attacks showed that action tags can be hijacked.

# ❌ Mutable tag β€” can be hijacked if the action repo is compromised
uses: azure/login@v2

# βœ… Pinned to specific commit SHA β€” immutable, tamper-proof
uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a  # v2.3.1

Complete supply chain integrity requires all three:

  1. OIDC β€” protects deployment credentials (no secrets to steal)
  2. SHA pinning β€” protects workflow actions (no tags to hijack)
  3. Attestation β€” protects build artifacts (Exercise 2)

Step 4: Run the OIDC Workflow

# Trigger the OIDC workflow
gh workflow run build-deploy-oidc.yml --ref main

# Watch the run
gh run watch

Step 5: Verify OIDC Token Exchange

After the workflow completes, inspect the logs:

# Get the latest run ID
RUN_ID=$(gh run list --workflow=build-deploy-oidc.yml --limit 1 --json databaseId --jq '.[0].databaseId')

# View the login step logs
gh run view $RUN_ID --log | grep -A 5 "Azure Login"

Look for: Login successful using OIDC β€” this confirms the federated token exchange worked.


Step 6: BEFORE vs. AFTER Comparison

Aspect BEFORE (Static Secret) AFTER (OIDC)
Authentication Long-lived JSON credential Short-lived OIDC token (~10 min)
Secret storage secrets.AZURE_CREDENTIALS No secrets β€” only public identifiers (vars.*)
Rotation Manual, error-prone Automatic β€” new token every run
Blast radius if leaked Full access until rotation Token expired within minutes
Audit trail β€œService principal logged in” β€œGitHub Actions workflow X on repo Y, branch Z, commit SHA”
Revocation Delete/rotate service principal secret Remove federated credential (instant)
Scope control No scoping β€” full access for anyone with secret Subject claim scoped to repo + branch + environment
Action protection N/A Recommend: Pin actions to commit SHAs

Step 7: Clean Up the Static Secret

# Now that OIDC is working, the static secret is no longer needed
gh secret delete AZURE_CREDENTIALS

Key Insight

OIDC is not a convenience feature. It is a supply chain integrity control that eliminates an entire class of attack β€” stolen deployment credentials. Every workflow run gets a unique, short-lived, auditable token tied to a specific repository, branch, and commit.

Tip: Run scripts/verify-exercise1.sh to validate your Exercise 1 completion.

← β†’ to navigate between steps