CAMPUX Cloud Bootcamp Phase Three · Class Twenty-Three
Phase Three — DevOps Core
Reading 40 min · Drills 6 · 2 Labs
Aligned to AZ-400
Class Twenty-Three

Actions → Azure with OIDC

The pipeline needs the keys to Azure, and the obvious move — store a credential in a secret — is the one this class exists to talk you out of; the better answer stores nothing at all.

§1

The old way, and why it should scare you

Here is how most teams first connect a pipeline to Azure, and how many still do. Create a service principal — the Class Nine robot account. Generate a client secret for it. Grant it Contributor on the subscription. Paste the secret into GitHub. Done in ten minutes, and it works. Now count what you have actually built: a permanent credential with the power to create and destroy most of your Azure estate, sitting in a third-party system, readable by every workflow in the repository, copied — be honest — into a teammate's password manager "just in case", and stamped with an expiry date that will surprise you at the worst possible moment.1

Every property of that arrangement ages badly. The secret does not expire when the project pauses; it waits. Rotation is a calendar reminder somebody snoozes, and the day it finally expires the deploy fails at release time. Anyone who can edit a workflow can, per Class Twenty-Two's Lab 2, exfiltrate the value past masking with one line of encoding. And when a security review asks the simple question — who has these keys, and where have they been? — the honest answer is a shrug. The problem is not carelessness. The problem is the design: a long-lived secret, stored outside your tenant, whose blast radius is your subscription. What this class replaces is not a bad habit but a bad architecture.

OIDC federation
An arrangement where Entra ID is configured to trust tokens issued by GitHub's identity provider — so a workflow run proves its identity with a short-lived, signed token minted for that run, exchanges it for Azure access, and stores no credential anywhere.
§2

The handshake — tokens, trust, nothing stored

OpenID Connect turns the problem inside out. Instead of the workflow holding a credential, the workflow is the credential: GitHub's identity provider signs a statement about the run — this repository, this branch, this environment — and Entra ID, which you have configured to trust such statements, exchanges it for a short-lived Azure token. Five steps, all inside one job, nothing stored before and nothing left after:

1 · The run asks
The job requests an OIDC token from GitHub's identity provider — allowed because the workflow declares permissions: id-token: write.
2 · GitHub signs
GitHub mints a signed JWT describing the run: which repository, which branch or environment, which event. This is the subject claim — the run's identity papers.
3 · Entra checks
The token goes to Entra ID, which checks it against the federated credential on your app registration: right issuer, right subject, right audience. No match, no entry.
4 · Entra issues
On a match, Entra returns a short-lived access token for the service principal — valid for this job, then gone.
5 · The job works
The azure/login action holds that token; every az command in the job runs with exactly the RBAC the principal was granted in Class Eight fashion.
Figure 13 — The OIDC handshake between GitHub, Entra ID, and Azure, with the stored-secret path struck through GitHub workflow run Proves who it is Entra ID federated credential Checks the papers Azure subscription Grants only its RBAC signed token access, minutes only scoped by RBAC client secret stored in GitHub, Contributor forever the old way — nothing travels this road anymore
Figure 13 — Three actors, one handshake, no stored key GitHub signs a statement about the run; Entra ID checks it against the federated credential and answers with minutes of access; Azure obeys its RBAC. The dashed road below is the old design — a permanent secret held outside the tenant — struck through because nothing needs to travel it.

Sit with what changed. There is no secret to leak, so Class Twenty-Two's echo-and-base64 attack retrieves nothing. There is no expiry cliff, because nothing is stored long enough to expire. Rotation is not a chore that gets skipped; it is the design — every job gets a fresh token and every token dies with its job. And the security review question flips from unanswerable to trivial: the trust relationship is a configuration object in your tenant, inspectable, auditable, and revocable in one delete.

§3

Federated credentials and the subject claim

The setup is three Azure-side objects and one workflow block. On the Azure side: an app registration (the Class Nine service principal), a federated credential attached to it, and an RBAC role assignment scoped as tightly as Class Eight taught. The federated credential is small enough to read in full:

# the trust object on the app registration
Issuer:   https://token.actions.githubusercontent.com
Subject:  repo:campux/campux-platform:environment:production
Audience: api://AzureADTokenExchange

The subject is where all the security lives. It is a filter over which runs may become this principal: repo:ORG/REPO:environment:NAME admits only jobs running in that environment — which means only jobs that have passed that environment's protection rules; repo:ORG/REPO:ref:refs/heads/main admits only runs on the main branch; repo:ORG/REPO:pull_request admits any pull request event, which is almost never what production should accept. Write the subject as narrowly as the job allows, because everything the token can do, anyone who can satisfy the subject can do. The workflow side is four lines and one action:

# deploy.yml (excerpt) — note what is absent: any secret value
permissions:
  id-token: write
  contents: read

steps:
  - uses: azure/login@v2
    with:
      client-id: ${{ vars.AZURE_CLIENT_ID }}
      tenant-id: ${{ vars.AZURE_TENANT_ID }}
      subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

Look closely at those three values: a client id, a tenant id, a subscription id. All three are identifiers, not credentials — knowing them grants nothing, which is why they can live in plain vars rather than secrets.2 The thing that grants access is the signed token minted at run time, and no line of this file contains it. This is the first deploy pipeline you have seen where a full leak of the repository — code, workflow, variables, logs — hands an attacker nothing they can log in with.

§4

Environments and approvals — the human gate

OIDC answers "how does the machine prove who it is?" One question remains: who decided this deploy should happen at all? That is what Class Twenty-Two's environments were built for, and OIDC snaps into them precisely. Give the production environment a required-reviewers rule, and point production's federated credential at environment:production. Now trace what a deploy to production requires: the job must declare environment: production; declaring it stops the job until a named human approves; only after approval does the run carry the environment:production subject; and only that subject satisfies the federated credential that Entra checks.

Nothing stored, nothing to steal.

The gate and the key are the same object. A compromised workflow on a feature branch cannot reach production's principal, because its token's subject says ref:refs/heads/feature-x and Entra shrugs. An impatient engineer cannot skip the approval, because skipping the environment means never earning the subject that unlatches the credential. Compare that with the old way, where the client secret worked from any branch, any fork with access, any laptop, at any hour — and the approval step was a custom in front of a door that was never actually locked. With OIDC federation the politeness is the lock; there is no path around the gate because the gate is what mints the key.

Case File · Campux Retail

The page interviewers get shown

campux-platform deploys itself, and stores no keys

The Class Twenty-Two validate workflow gets its sibling: deploy.yml. On every merge to main, the pipeline logs into Azure through OIDC and deploys the Bicep to the non-production resource group — no human, no wait. Then the same workflow requests environment: production and stops. A named reviewer reads the what-if posted on the run, clicks approve, and only then does a production-scoped token exist at all. Two service principals, two federated credentials — non-prod trusts ref:refs/heads/main, production trusts environment:production — each holding Contributor on its own resource group and nothing else.

Run gh secret list on the repository and the answer is empty. That empty list is the Class Nine promise kept — "a later class connects a pipeline with nothing stored at all" — and it is the artifact worth walking an interviewer through slowly: merge deploys non-prod, approval mints production access, and there is no credential to steal anywhere in the system. Candidates describe pipelines; you can show one whose security a leak cannot break. Forty stores never notice, which is the point.

§5

The Class Nine promise, kept

Step back and see the arc, because interviewers who probe this topic are really probing whether you see it. Class Nine drew the line: humans get accounts, machines get identities, and the best machine identity is one with no credential to manage — that was managed identity, and it worked only for things running inside Azure. A GitHub runner lives outside, so for five classes the question hung open: what about the machine that is not ours?

OIDC federation is the same idea stretched across an organisational boundary. Managed identity works because Azure vouches for its own compute; federation works because Entra is told, precisely and revocably, to accept GitHub's vouching for a specific repository, branch, or environment. Same principle — trade stored credentials for attested identity — new trust anchor. And the pattern is bigger than this class: the same federated-credential mechanism connects Kubernetes workloads, other clouds, and other CI systems to Entra without a stored key.3 When an interviewer asks "how does your pipeline authenticate?", the strong answer walks the Figure 13 handshake and then names the general rule. The weak answer says "we have a secret for that" — and now you know exactly what follows from it: an unrotated key, an unanswerable audit, and a door that was never really locked.

Watch · Microsoft Learn

The official pages, and a CAMPUX overview

GitHub's concept page first, then Microsoft's setup walkthrough
Docs · Both sides of the handshake

GitHub Docs — Configuring OpenID Connect in Azure
docs.github.com/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure

GitHub Docs — About security hardening with OpenID Connect
docs.github.com/actions/concepts/security/openid-connect

CAMPUX overview video

A walkthrough of the Figure 13 handshake, the federated credential being created in the portal, and a deploy pausing at the production gate until a human approves will live here. Video to be added.

Lab 1 · Federate & log in

A pipeline that logs into Azure with nothing stored

~20 minutes · Azure CLI + a GitHub repository you own · rights to create an app registration

Build the whole §3 chain: app registration, federated credential, RBAC scope, and a workflow that proves the login worked. Replace <you> with your GitHub username throughout.

  1. Create the identity and a tightly-scoped role. In Cloud Shell or your terminal:

    az group create --name rg-oidc-lab --location eastus
    az ad app create --display-name campux-oidc-lab
    # note the appId from the output, then:
    az ad sp create --id <appId>
    az role assignment create --assignee <appId> \
      --role Reader --resource-group rg-oidc-lab
    What to notice: Reader, on one throwaway resource group — Class Eight discipline. The lab works identically with a tiny role because OIDC changes how you authenticate, not what you are authorized to do.
  2. Attach the federated credential — the trust object itself:

    az ad app federated-credential create --id <appId> --parameters '{
      "name": "github-main",
      "issuer": "https://token.actions.githubusercontent.com",
      "subject": "repo:<you>/hello-github:ref:refs/heads/main",
      "audiences": ["api://AzureADTokenExchange"]
    }'
    What to notice: the subject names one repository and one branch. A run from any other repo or branch presents papers that do not match, and Entra refuses the exchange. This one string is the lock.
  3. In the repository, store the three identifiers as variables (Settings → Secrets and variables → Actions → Variables): AZURE_CLIENT_ID (the appId), AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID (both from az account show).

    What to notice: variables, not secrets — deliberately. All three are public-shaped identifiers; the exercise is feeling that nothing sensitive exists to store.
  4. Add .github/workflows/oidc-login.yml on main, run it from the Actions tab, and read the log:

    name: OIDC login test
    on: workflow_dispatch
    permissions:
      id-token: write
      contents: read
    jobs:
      login:
        runs-on: ubuntu-latest
        steps:
          - uses: azure/login@v2
            with:
              client-id: ${{ vars.AZURE_CLIENT_ID }}
              tenant-id: ${{ vars.AZURE_TENANT_ID }}
              subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
          - run: az group show --name rg-oidc-lab --query properties.provisioningState
    On screen: "Login successful", then "Succeeded" from a real az call — a GitHub runner reading your Azure tenant with zero stored credentials. Delete the resource group and app registration when done: az group delete --name rg-oidc-lab --yes --no-wait and az ad app delete --id <appId>.
Note · if the login step fails with AADSTS700213 or a subject mismatch, compare the run's branch and repo against the federated credential's subject character by character — the match is exact, and case matters. That error is the lock working; make the papers match the door.
Lab 2 · The human gate

Make production wait for a person

~10 minutes · continues Lab 1 · repository Settings access

Wire an environment with a required reviewer, scope a second federated credential to it, and feel the job stop until you approve it.

  1. In the repository: Settings → Environments → New environment → name it production. Under protection rules, tick Required reviewers and add yourself.

    What to notice: the environment is just a name plus rules — the power comes from what you attach to the name next.
  2. Add a second federated credential on the Lab 1 app, trusting the environment instead of the branch:

    az ad app federated-credential create --id <appId> --parameters '{
      "name": "github-prod-env",
      "issuer": "https://token.actions.githubusercontent.com",
      "subject": "repo:<you>/hello-github:environment:production",
      "audiences": ["api://AzureADTokenExchange"]
    }'
    What to notice: subjects are exact-match, so a credential per context is normal — one for main, one for the production environment, each admitting only its own kind of run.
  3. In the workflow, add environment: production to the job (directly under runs-on:), commit to main, and run it again.

    On screen: the run starts and immediately pauses — a yellow "Waiting for review" banner names you as the gate. Nothing has executed; no token exists yet.
  4. Click Review deployments → approve → watch the job finish. Then clean up as in Lab 1.

    The lesson: your click did not merely release a queued job — it is what let the run carry the environment:production subject that the federated credential demands. The approval and the credential are one mechanism. That pause-then-proceed is the exact experience a production deploy at Campux has from now on.
On the job

No expiry to babysit, ever again

You · Cloud Engineer · a pipeline secret is expiring

The service principal secret in CI is about to expire and take deploys down with it. Instead of rotating it again, you federate the pipeline to Azure with OIDC — the workflow now trades a short-lived token for an Azure token at run time, and the stored secret is deleted. The secret list comes back empty, and there is no expiry left to babysit ever again.

Class Twenty-Three

Examination

Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.

Drill 01Recall · flow order
Put the OIDC handshake in order. Which sequence is correct?
Marked

B — and the direction matters more than the steps. Identity flows from GitHub outward: the run proves what it is with a token minted for that run, and Entra converts proof into minutes of access. A has the old architecture wearing new words — the moment anything durable is stored in GitHub, you have rebuilt the client secret. C confuses the human gate (an environment rule) with authentication, and no click ever "generates a password". D imagines Entra reads YAML; it never sees your workflow, only the signed claims about it. If you can narrate B from memory, you can answer the interview question this class exists for.

Drill 02Recall · trust
What, exactly, makes Entra ID accept a token from a GitHub workflow run?
Marked

C. The federated credential is a three-part filter sitting on the app registration: issuer (GitHub's token service and nobody else's), subject (this repo, this branch or environment), audience (the Azure exchange). All three match or the exchange is refused — there is no partial credit. A smuggles the stored secret back in, which is precisely what federation eliminates. B would be catastrophic if true; visibility has nothing to do with trust. D describes nothing that exists — the trust is an object in your tenant, created, audited, and deleted by you, which is why the auditor conversation in Situation 01 goes so well.

Drill 03Select three
Which three are genuine advantages of OIDC federation over a client secret stored in GitHub?
Marked

Nothing stored, short-lived tokens, subject-scoped trust. The two false options fail the same way: they credit authentication with jobs that belong elsewhere. RBAC is untouched by OIDC — federation changes how the principal proves itself, never what it may do, and the Class Eight role assignment matters exactly as much as before. And no authentication scheme reviews code; a perfectly federated pipeline will deploy a perfectly broken template with flawless credentials. Keep the layers separate — identity, authorization, quality — because the drill's wrong answers are, in the wild, the assumptions behind real incidents.

Drill 04Spot the error
This federated credential is for the principal that holds Contributor on Campux's production resource group. One line is a serious mistake. Which?
# federated credential — campux production deployer
1.  Name:     github-prod
2.  Issuer:   https://token.actions.githubusercontent.com
3.  Subject:  repo:campux/campux-platform:pull_request
4.  Audience: api://AzureADTokenExchange
5.  # principal role: Contributor on rg-campux-prod
Marked

Line three. The pull_request subject admits every run triggered by any pull request — which is to say, code that has not merged, not passed review, and possibly arrived five minutes ago on a branch nobody has read. Anyone who can open a PR against this repository can have their workflow run become the production Contributor. The whole point of the subject is to make the trust narrow; this one makes it a revolving door in front of the most powerful principal in the estate.

The fix is the §4 pattern: repo:campux/campux-platform:environment:production, so only a job that passed the production environment's required approval carries matching papers. The other lines are correct as written — the issuer is GitHub's token service (A), the audience is the fixed exchange value, not a scope (C), and the name is a label with no security meaning (D). One line, read carelessly, is the difference between a locked gate and an open one — which is why workflow and credential diffs get Class Twenty-Two's four-question review, every time.

Situation 01Write before you reveal
An auditor reviewing Campux's release process asks the standard question: "Where are your deployment credentials stored, who can access them, and when were they last rotated?" Answer as the engineer who built the pipeline.
The honest answer to "where are they stored" is the whole story. Say it, then prove it.
Reasoning

Lead with the answer that sounds impossible, then make it inspectable. "They are not stored anywhere" reads as evasion to an auditor who has heard every dodge — so follow it in the same breath with the mechanism: each deployment authenticates with a token minted for that single run, exchanged with Entra ID under a federated trust, expiring in minutes. There is no vault entry, no GitHub secret, no password manager, because there is no long-lived credential in the system. Then answer the two follow-ups inside their question: "who can access them" becomes which runs can — exactly those matching the subject claims, which you can enumerate — and "when were they rotated" becomes every single run, by construction.

Then hand over evidence, not assurances. Auditors trade in artifacts. Show the federated credentials on the app registrations — issuer, subject, audience, readable in one screen. Show the RBAC assignments: non-prod's principal holds Contributor on its resource group, production's on its own, nothing subscription-wide. Show the production environment's required-reviewer rule and the deployment history of who approved what, when. Every claim you made in the first paragraph corresponds to an object they can inspect — which is the difference between a security posture and a security story.

Volunteer the boundary, because credibility lives there. Say what the design does not cover: a malicious change to the workflow file itself is still the live risk, which is why .github/ is CODEOWNERS-routed and review-required from Class Twenty-Two, and why the subject claims are environment-scoped so even a rogue workflow on a branch cannot reach production's principal. An auditor who hears you name your own residual risk unprompted upgrades every other answer you gave. This conversation is why the case file calls it the page interviewers get shown — the same answer wins both rooms.

Situation 02Write before you reveal
A teammate setting up a new repository's pipeline says: "The OIDC setup is fiddly — app registrations, federated credentials, subjects that have to match exactly. A client secret is one paste and it works. We'll rotate it quarterly, I promise. Ship now, harden later?" What do you say?
They are right about the twenty minutes. Price the other side of the trade.
Reasoning

Concede the true part first. They are right: the secret is one paste, and the federation is twenty fiddly minutes of ids and exact-match subjects — Lab 1 is honest about that. Denying the friction costs you the argument. The disagreement is about what each choice costs after setup day, and that comparison is not close: the secret starts a permanent liability — a stored, powerful, leakable value plus a rotation chore — while the federation, once built, is zero ongoing work. Fiddly-once beats dangerous-forever.

Then price "I promise" honestly, without making it personal. Quarterly rotation is a calendar reminder competing with every deadline for the rest of the repository's life, owned by whoever has not left yet. Class Twenty-Two showed a one-line base64 walking a secret past masking; every workflow edit between now and forever is a chance for that line. And "harden later" has a specific failure mode: later is when the pipeline works, nobody wants to touch it, and the temporary secret gets a birthday. The realistic comparison is not OIDC versus rotated-secret — it is OIDC versus a secret that outlives three teammates.

Close by collapsing the cost you conceded. The fiddly part is already done once, in campux-platform — the app registration pattern, the subject formats, the working azure/login block are all sitting in a repository they can copy from. Offer the twenty minutes: pair on it today, template it for the next repository, and the "ship now" pressure is satisfied by shipping the good design now. When the fast path and the right path are twenty minutes apart, the job is to shrink the twenty minutes — not to institutionalise the wrong path with a promise stapled to it.

Examination record · first attempt
0/4
Class Twenty-Three · Complete
Retain this much

Five things worth carrying out of this class

  1. A client secret in GitHub is a bad architecture, not a bad habit: long-lived, stored outside the tenant, readable by any workflow, and rotated by a promise.
  2. The OIDC handshake: the run requests a signed token from GitHub, Entra checks issuer/subject/audience against the federated credential, and answers with minutes of access. Nothing stored before, nothing left after.
  3. The subject claim is where the security lives — environment:production admits only approved runs; pull_request admits anyone who can open a PR. Write it as narrow as the job allows.
  4. client-id, tenant-id, and subscription-id are identifiers, not credentials — they live in vars, and a full repository leak hands an attacker nothing to log in with.
  5. Environments plus environment-scoped credentials make the approval and the key one mechanism: the gate mints the subject, and only the subject unlatches production. Managed identity's principle, stretched across the GitHub boundary — the Class 9 promise, kept.
Notes
  1. The client-secret pattern is not wrong in every universe — it predates federation, and you will inherit pipelines built on it. The professional move on encountering one is not scorn but a migration ticket: the switch to OIDC changes the login step and deletes a secret, and the rest of the pipeline never notices. Knowing the old way well enough to replace it gracefully is itself an employable skill.
  2. Treat "identifiers are safe to publish" as a rule about authentication, not an invitation to broadcast: a tenant id or subscription id grants no access, but it does help an attacker aim phishing or enumerate targets, so teams reasonably keep them out of public view anyway. The load-bearing claim is narrower and solid — none of the three values in the login block can be exchanged for access without a token whose issuer, subject, and audience match your federated credential.
  3. Federation can also point at a user-assigned managed identity instead of an app registration — same issuer/subject/audience trust, one less object to manage, and increasingly the recommended default. The concepts in this class transfer unchanged; if your shop uses that variant, only the Azure-side noun is different. As ever with identity features, check current docs before an interview — this corner of Azure moves quickly.