Your pipeline needs to deploy to Azure. The naive way: store Azure credentials as a GitHub secret. The correct way: OIDC federated identity — no credentials stored anywhere. This lesson teaches you why and how.

The Problem with Stored Credentials

❌ Old Way: Client Secret in GitHub GitHub Secret: AZURE_CLIENT_SECRET = "abc...xyz" Problems: • Secret can leak (logs, forks, compromised action) • Expires → pipeline breaks → manual rotation • Anyone with repo access has Azure access ✅ OIDC: No Secret Stored GitHub proves identity → Azure grants short-lived token Benefits: • No secret to leak (nothing stored) • Tokens are short-lived (minutes, not years) • Scoped per repo/branch/environment

How OIDC Works (The Flow)

GitHub Actions Azure AD Azure Resource Generate JWT "I am repo:user/cicd-mastery:ref:refs/heads/main" Verify JWT signature Check federated cred Access token (short-lived, ~1 hour) Deploy (using access token) ✅ Deployed! 🔑 No secret was stored or transmitted. GitHub proved its identity; Azure trusted it.
OIDC: GitHub says "I am this workflow." Azure verifies the claim and issues a short-lived token. Zero stored secrets.
The federated credential is the trust relationship. It says: "Azure, trust JWTs from GitHub Actions that claim to be repo user/cicd-mastery on branch main." This is scoped — a different repo or branch can't impersonate your pipeline.

🏋️ Hands-On: Set Up Azure OIDC

Step 1: Create an Azure AD App Registration

# Login to Azure
az login

# Create app registration
az ad app create --display-name "github-actions-cicd-mastery"

# Get the App (Client) ID
APP_ID=$(az ad app list --display-name "github-actions-cicd-mastery" \
  --query "[0].appId" -o tsv)

# Create service principal
az ad sp create --id $APP_ID

# Get IDs you'll need
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)

echo "Client ID: $APP_ID"
echo "Tenant ID: $TENANT_ID"
echo "Subscription ID: $SUBSCRIPTION_ID"

Step 2: Add Federated Credentials

# Trust GitHub Actions from your repo's main branch
az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "github-main-branch",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:YOUR_USERNAME/cicd-mastery:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

# Trust your "production" environment
az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "github-env-production",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:YOUR_USERNAME/cicd-mastery:environment:production",
  "audiences": ["api://AzureADTokenExchange"]
}'

Step 3: Grant Azure Permissions

# Contributor on a resource group (scope it tight!)
az group create --name rg-cicd-mastery --location eastus

RG_ID=$(az group show --name rg-cicd-mastery --query id -o tsv)

az role assignment create \
  --assignee $APP_ID \
  --role "Contributor" \
  --scope $RG_ID

Step 4: Add to GitHub (NOT secrets — just IDs)

Go to repo → Settings → Secrets and Variables → Actions → Variables:

NameValueType
AZURE_CLIENT_IDYour App IDSecret*
AZURE_TENANT_IDYour Tenant IDSecret*
AZURE_SUBSCRIPTION_IDYour Subscription IDSecret*

*These aren't truly secret (they're in every Azure portal URL), but storing as secrets is conventional and prevents accidental exposure in logs.

Step 5: Use in Workflow

permissions:
  id-token: write    # REQUIRED for OIDC!
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      
      - name: Verify access
        run: az account show

Scoping: Why Federated Credentials Are Precise

Federated Credential Subjects (who can authenticate) Branch-scoped repo:user/repo:ref:refs/heads/main Environment-scoped repo:user/repo:environment:production Tag-scoped repo:user/repo:ref:refs/tags/v* A workflow on branch feature-x CANNOT authenticate if the credential is scoped to main. This means: only YOUR pipeline, on YOUR branch, can access YOUR Azure resources.

Common Pitfall: Missing permissions

If you forget permissions: id-token: write, the OIDC token request will silently fail and azure/login will error with "AADSTS700024" or "Failed to get federated token." This is the #1 debugging issue with OIDC.

🧠 Recall Check

  1. What is stored in GitHub Secrets when using OIDC? (Hint: it's NOT a password)
  2. What permission must be set at the workflow level for OIDC to work?
  3. What does the "subject" field in a federated credential control?
  4. Why is OIDC more secure than a client secret?
Reveal answers
  1. Only IDs — Client ID, Tenant ID, Subscription ID. These are identifiers (like a username), not passwords. No credential is stored.
  2. permissions: id-token: write — this allows the workflow to request a JWT from GitHub's OIDC provider.
  3. Who can authenticate. E.g., repo:user/repo:ref:refs/heads/main means only workflows running on the main branch of that specific repo can get a token.
  4. No secret exists to leak, expire, or rotate. Tokens are short-lived (~1 hour) and scoped. Even if intercepted, they expire quickly and only work for the specific resource scope granted.
You now have credential-free Azure authentication. Every future deployment lesson will use OIDC. This is the foundation of secure CI/CD — no long-lived secrets, ever. Next: deploying a real application to Azure App Service.