CAMPUX Cloud Bootcamp Lab · CI/CD ← All labs
Hands-On Lab · Intermediate
~40 min · Free tier · Cloud Shell + GitHub
CLI & Actions · torn down at the end
CI/CD · Pipelines

GitHub Actions to Azure, with zero stored secrets.

The long-lived client secret pasted into a pipeline is the credential most likely to leak — and the one interviewers ask about. Replace it with OIDC: your workflow requests a short-lived token at run time, Azure trusts the request because of who is asking, and nothing sensitive is ever stored.

Fig. 1 · GitHub Actions → Azure with OIDC
OIDC token requestshort-lived tokenGitHub Actionsworkflow runFederated credentialtrusts this repo + branchAzurescoped, least-privilege roleno stored password — a fresh token per run
● Screen walkthrough Not yet recorded · ~9 min
Reel · 00:00 / 09:00

Build the zero-stored-secrets pipeline and drive it from a red run to a green Azure deploy.

Placeholder — the page below stands alone until the reel lands
Why

Trust the caller, not a saved password

The old way to let a pipeline into Azure was to create a service principal, generate a client secret, and paste it into your CI system. That secret is long-lived, copyable, and a headache to rotate — exactly the thing that ends up in a breach post-mortem. OpenID Connect replaces it with federation: GitHub issues each workflow run a signed token describing who it is (this repo, this branch), and you configure Azure to trust tokens matching that description. No secret is stored anywhere; the token that is minted lasts minutes and cannot be reused. This is now the recommended pattern in every serious posting, and it is genuinely simple once you have done it once.

You will create an app registration for the pipeline, grant it a role on a single resource group, tell Azure to trust your repository's main branch, and then watch a workflow deploy with nothing but three non-secret IDs.

A stored secret is a liability with a shelf life. A federated token is a handshake that expires in minutes.

Before you begin — one-time setup

You need a free Azure account, the Azure CLI (az), then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in.

What you need

A GitHub repository you own (an empty one is fine), plus Azure Cloud Shell (Bash) for the az commands. The workflow file and a tiny Bicep template are in github.com/kloudcaptain/campux-labs under lab-github-actions-oidc. Replace OWNER/REPO below with your repository throughout.

Setup

An identity for the pipeline

Create an app registration and its service principal — the identity your workflow will act as — then capture the IDs you will need.

# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

SUB_ID=$(az account show --query id -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)

APP_ID=$(az ad app create --display-name "campux-gh-oidc" --query appId -o tsv)
az ad sp create --id "$APP_ID"

echo "client-id (appId): $APP_ID"
echo "tenant-id:         $TENANT_ID"
echo "subscription-id:   $SUB_ID"
Checkpoint You have three IDs printed back. None of them is a secret — they identify the app but grant nothing on their own. Access comes from the role you assign next, and sign-in comes from the federation you configure after that.
Step 1

Grant a scoped, least-privilege role

Make a resource group for the pipeline to deploy into, and give the app Contributor only on that group — never the whole subscription.

RG="campux-lab-oidc-rg"
az group create -n "$RG" -l eastus

az role assignment create \
  --assignee "$APP_ID" \
  --role Contributor \
  --scope "/subscriptions/$SUB_ID/resourceGroups/$RG"
Checkpoint The assignment returns a JSON object with "roleDefinitionName": "Contributor" scoped to your resource group. If the pipeline is ever misused, the blast radius is one resource group — the least-privilege habit that keeps a mistake from becoming an incident.
Step 2

Tell Azure which workflow to trust

Add a federated identity credential. The subject is the exact identity GitHub will present; here it means "a run of OWNER/REPO on the main branch." The audience is the fixed value Azure expects.

az ad app federated-credential create --id "$APP_ID" --parameters '{
  "name": "gh-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:OWNER/REPO:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'
Checkpoint The credential is created with your subject. Azure will now mint an access token for any GitHub run whose token matches that subject exactly — and refuse everything else. A workflow on a different branch, or in a fork, does not match and is denied.
Step 3

Put the three IDs in the repo — as variables, not secrets

Because none of the three is sensitive, store them as repository variables. (You can use the GitHub UI under Settings → Secrets and variables → Actions → Variables, or the gh CLI.)

# with the GitHub CLI, from a clone of your repo
gh variable set AZURE_CLIENT_ID --body "$APP_ID"
gh variable set AZURE_TENANT_ID --body "$TENANT_ID"
gh variable set AZURE_SUBSCRIPTION_ID --body "$SUB_ID"
Checkpoint Three repository variables exist. Note what is not here: no AZURE_CLIENT_SECRET. That absence is the entire point of the lab.
Step 4

The workflow

Two things make OIDC work in a workflow: the id-token: write permission (so the run may request a token) and azure/login with the three IDs. Commit this as .github/workflows/deploy.yml.

name: deploy
on:
  push:
    branches: [ main ]

permissions:
  id-token: write        # lets the run request an OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy a storage account
        uses: azure/cli@v2
        with:
          azcliversion: latest
          inlineScript: |
            az deployment group create \
              --resource-group campux-lab-oidc-rg \
              --template-uri https://raw.githubusercontent.com/kloudcaptain/campux-labs/main/lab-github-actions-oidc/bicep/main.bicep
Checkpoint Commit and push to main. On the Actions tab the run goes green, and the Azure login (OIDC) step logs Login successful without any secret in the logs or the repo. You just deployed to Azure on a token that was minted for this run and has already expired.
Verify

Prove it was real — and secret-free

Back in Cloud Shell, confirm the workflow actually created the resource, and confirm there is no secret behind it.

az resource list -g campux-lab-oidc-rg --query "[].{name:name,type:type}" -o table
az ad app credential list --id "$APP_ID" --query "length(@)"   # -> 0 client secrets
Checkpoint The resource list shows the storage account the pipeline created, and the credential count is 0 — the app has no password at all. Authentication came entirely from the federated trust you configured. That is the sentence to say in an interview.
Down

Tear it down

Delete the resource group and the app registration so nothing lingers.

az group delete -n campux-lab-oidc-rg --yes
az ad app delete --id "$APP_ID"
az group exists -n campux-lab-oidc-rg      # -> false
Checkpoint The group is gone and the app registration is deleted. You may also remove the three repository variables. Your tenant and subscription are back where they started.
End

What you can now honestly claim

You federated GitHub Actions to Microsoft Entra ID with OIDC, scoped the pipeline's identity to a single resource group, and deployed to Azure from a workflow that stores no secret — verifying afterwards that the app registration has zero credentials. That is precisely the "secure, auditable, no-secrets CI/CD" line that appears in nearly every cloud-engineer posting, and now you have done it, not just read about it. Pair it with the Terraform lab and you have the two halves of how modern teams ship infrastructure: code in a repo, deployed by a pipeline that no one had to trust with a password.

Footnotes
  1. The subject must match GitHub's token exactly. For a branch it is repo:OWNER/REPO:ref:refs/heads/BRANCH; to gate deploys behind a GitHub Environment instead, use repo:OWNER/REPO:environment:NAME and add a second federated credential. A mismatch here is the single most common reason an OIDC login is refused.
  2. The three IDs are safe to expose because they are identifiers, not credentials — possessing them grants nothing without both a role assignment and a matching federated trust. Storing them as variables rather than secrets is a deliberate signal that they are not sensitive.