The old way ships a password with every deploy. Workload Identity Federation makes that the last problem you'll ever have.
For a long time, I thought I was doing CI/CD security right.
App registration? Created. Client secret? Generated. GitHub secret? Stored. Pipeline? Green.
Clean. Neat. Ticking every box.
What I hadn't noticed: I had handed GitHub a master key to my Azure environment, sitting in a secret store, expiring every 12 months, and one misconfigured permissions screen away from a bad day.
Most Azure deployments work exactly like this. Most teams have never thought twice about it. And honestly? Neither had I, until I asked what felt like a stupid question:
Why are we giving machines passwords at all?
That question leads somewhere much more interesting than I expected. This post is about what I found, and why the answer will change how you think about service-to-service authentication entirely.
The thing everyone gets comfortable with (that they shouldn't)
Here is how almost every GitHub-to-Azure deployment works today.
You create an Entra ID app registration. You generate a client secret. You copy it into a GitHub Actions secret, usually named something like AZURE_CLIENT_SECRET. The pipeline logs in with it, gets access, deploys.
This feels fine. It's recommended in plenty of tutorials. It works.
But here's what you've actually created:
| What you think you have | What you actually have |
|---|---|
| A secure deployment pipeline | A long-lived password |
| Credentials safely stored | Sitting in an external system |
| Auditable access to Azure | That must be rotated manually |
| And can leak silently |
Long-lived secrets are a category of risk, not just a configuration detail. They can be exposed in logs. They can survive past team membership changes. They expire silently and break pipelines at the worst moment.
More fundamentally: you shouldn't need to give a pipeline a secret at all.
That insight sounds small. But follow it far enough, and it rewrites the entire authentication model.
The shift that changes everything: trust instead of secrets
Consider how you trust a person vs. a machine.
When a person from GitHub's engineering team shows up at your office, they don't hand over a password. They show ID. The ID proves where they're from, who they are, and when it was issued. It expires. It can be revoked. It proves identity without transferring a secret.
Workload Identity Federation applies this same logic to pipelines.
Instead of storing a secret, you teach Azure to recognize GitHub's identity and accept a short-lived proof token that GitHub generates fresh for each run.
No secret is stored. No secret is transmitted across systems. No secret can leak.
Here's the flow that replaces your entire secrets setup:
1. GitHub Action runs.
Your workflow triggers as normal, push to main, PR merge, manual dispatch.
2. GitHub issues an OIDC token.
A short-lived, cryptographically signed proof of identity: this workflow, this repo, this branch, this commit.
3. Azure validates the token.
Entra ID checks: is this repo on my trusted list? Does the branch match? Is the token fresh?
4. Azure issues an access token.
If validation passes, Azure hands the workflow a scoped, time-limited access token for that run.
5. Deployment happens.
The workflow uses the token, deploys, finishes. The token expires. Nothing to clean up.
No secret ever leaves your Azure tenant. GitHub never holds a credential. You never rotate anything.
The setup takes about 5 minutes and the actual workflow change is three lines.
The real-world setup
Here's the minimal implementation. Two parts.
Part 1: Create the federated credential in Azure
Register an Entra ID application as normal. But instead of generating a client secret, add a federated credential.
You'll configure:
- Identity provider: GitHub
- Organisation and repository:
your-org/your-repo - Entity type: Branch, PR, tag, or environment
- Branch name:
main
az ad app create --display-name "github-actions-pipeline"
APP_ID=$(az ad app list --display-name "github-actions-pipeline" --query "[0].appId" -o tsv)
az ad sp create --id "$APP_ID"
az ad app federated-credential create \
--id "$APP_ID" \
--parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<org>/<repo>:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
This creates a trust policy that says: "I will accept tokens that come from this specific GitHub repo and branch. Nothing else."
No secret is generated. No certificate is created. You've configured identity trust, not credential transfer.
Part 2: Update the GitHub Actions workflow
name: Deploy to Azure
on:
push:
branches: [ main ]
permissions:
id-token: write # required, enables OIDC token generation
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Azure (no secret)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy
run: az group list
Notice what's missing:
- ❌ No
AZURE_CLIENT_SECRET - ❌ No certificate reference
- ❌ No long-lived credential of any kind
The three values you do pass — client ID, tenant ID, subscription ID — are not secrets. They're identifiers. Knowing them without a valid OIDC token gets an attacker exactly nowhere.
One permission flag (id-token: write) is the only thing that unlocks the entire flow.
What most teams don't think about
Here's the question worth asking once you've seen this model: why did we ever use secrets?
The honest answer is that OIDC federation at this level is relatively new. Most tooling defaulted to client secrets because that's what the ecosystem supported. Tutorials taught it. Templates shipped with it. It became the default.
But the pattern of "give a machine a password" is one of the oldest and most persistent sources of credential exposure in cloud infrastructure. It's not a niche problem, it's how most supply chain attacks start.
Workload Identity Federation doesn't just improve security incrementally. It removes an entire class of risk from the equation.
No secret → nothing to rotate → nothing to expire → nothing to leak → nothing to audit for accidental exposure.
That's not a small upgrade. That's a category change.
When not to use this
This approach requires the external system to support OIDC token issuance. GitHub does. GitLab does. Most modern CI platforms do. But legacy pipelines, internal runners, or tools built before OIDC federation was common may not. In those cases, managed identities (if the runner is Azure-hosted) or carefully scoped client secrets remain the fallback, not the goal.
The short version
The old way Generate a client secret in Azure. Store it in GitHub. Rotate it manually. Hope it never leaks. Repeat every 12 months.
The new way Configure Azure to trust GitHub's identity. GitHub proves who it is per run. No secret is ever created, stored, or transferred.
What changes in your workflow Add id-token: write to permissions. Remove AZURE_CLIENT_SECRET. That's it.
What you eliminate Secret rotation. Expiry surprises. Leak exposure. An entire category of credential risk, gone by design.
You're not giving GitHub a key to Azure. You're teaching Azure to recognise GitHub's face. Once that clicks, going back to client secrets feels like leaving your front door key under the mat.
The setup takes under 5 minutes. The risk reduction is permanent. It's one of the highest-leverage security changes you can make to a modern deployment pipeline and most teams still haven't done it.
Originally published on Techworld of Florian

Top comments (0)