OIDC & Credential-Free Deployment
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_CREDENTIALSis 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:
- Read back the subject claim you configured β is it scoped to
main? β - 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. - 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/loginaction 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.1Complete supply chain integrity requires all three:
- OIDC β protects deployment credentials (no secrets to steal)
- SHA pinning β protects workflow actions (no tags to hijack)
- 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.shto validate your Exercise 1 completion.