Workload Identity Fundamentals and Credential Management in Microsoft Entra
In this article
Summary
Picture a workload with a nightly job, let's call it "Invoice-sync". What this workload does is it pulls finance data out of an ERP system and writes it into a SharePoint list using Microsoft Graph. Nothing about that job is unusual, which is exactly why it's a useful example. Its real risk isn't set by any one decision. It comes from three things happening together: how it proves it's really "Invoice-sync" every night, what it's actually allowed to do once Microsoft Entra ID believes it, and whether anyone is still paying attention to either of those a year after the job quietly went live. A strong credential attached to a permission far broader than the job needs is still a risk. A tightly scoped permission attached to a credential that leaked into a public repository is still a risk. The rest of this article follows "Invoice-sync" through each of those stages, how it's built, how it authenticates, what it can touch, and what changes when each is done correctly, instead of just done fast.
The Lifecycle, Where Things Go Wrong
Most non-human identity incidents don't trace back to one bad configuration. They trace back to a credential that was perfectly reasonable the day it was created and was never looked at again. Here's how that plays out for "Invoice-sync", stage by stage.
| Stage | What's supposed to happen | What happens to "Invoice-sync" |
|---|---|---|
| Provisioning | A credential is created with a short expiration and a named owner. | The engineer setting it up picks the default expiration and moves on, there's a deadline for the finance report, not for credential hygiene. |
| Storage | The credential lives in one managed location and is never copied. | It ends up in a pipeline variable, and a copy lands in a teammate's local .env file during troubleshooting, because that was the fastest way to unblock a failing run. |
| Runtime use | The job requests only the access it needs, when it needs it. | It authenticates the same way every night, with whatever permission was granted at setup, nobody has revisited it since. |
| Rotation | Secrets are rotated automatically, well before they expire. | Rotating it risks breaking the nightly finance load, so when the reminder comes due, someone extends the expiration date instead. |
| Retirement | When a job is retired, its credentials and permissions go with it. | Three years later, the ERP system is replaced, and "Invoice-sync" is turned off, but the app registration and its still-valid credential are left behind, because nothing tied them together. |
Table 1 - Describes the stages of credential Lifecycle
None of that required an attacker to be clever. It's what happens by default when nobody revisits a decision made under deadline pressure, which is exactly why an inventory has to be something you repeat, not something you finish.
How a Workload Proves Who It Is
Every night, "Invoice-sync" has to convince Entra ID that it really is "Invoice-sync" before it's allowed to touch anything. Under the hood, that's a single, standard exchange , an OAuth 2.0 client credentials grant (RFC 6749 §4.4), sent to Entra ID's token endpoint. What changes between the three options below is only one thing: what "Invoice-sync" presents as proof.
Option 1: A Client Secret
If "Invoice-sync" uses a client secret, it sends that secret straight to Microsoft Entra ID as plain text, alongside its client ID. Entra ID checks it against a stored hash and, if it matches, issues a token. This is just password authentication with a longer password: whatever value proves "Invoice-sync"'s identity is the same value that has to be stored somewhere reachable by the job. Anyone who finds that value , in a repo, a log, a config file , can authenticate as "Invoice-sync" with no further effort.
Option 2: A Certificate
If "Invoice-sync" uses a certificate instead, it never sends a secret at all. It builds a small JSON token, signs it with its private key, and sends the signature. Microsoft Entra ID already has the matching public key on file and uses it to check that signature:
{ "alg": "RS256", "typ": "JWT", "x5t": "<certificate thumbprint>" } { "iss": "<"Invoice-sync" client ID>", "sub": "<"Invoice-sync" client ID>", "aud": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", "exp": <10 min from now> } |
The private key that produces this signature can live inside Azure Key Vault and never leave it, Key Vault will sign the token on "Invoice-sync"' behalf without ever exposing the key. Whoever wants to forge this now needs access to a protected signing operation, not a string in a config file.
Option 3: Workload Identity Federation
If "Invoice-sync" runs as a GitHub Actions job, federation is the natural fit. GitHub already issues it a short-lived identity token for the run. Instead of "Invoice-sync" signing its own proof, it hands over that token, and Microsoft Entra ID checks it against a trust relationship set up in advance:
POST https://graph.microsoft.com/v1.0/applications/{app-object-id}/federatedIdentityCredentials { "name": ""Invoice-sync"-nightly", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:finance-org/"Invoice-sync":ref:refs/heads/main", "audiences": ["api://AzureADTokenExchange"] } |
Microsoft Entra ID confirms the token really came from GitHub, and that it's specifically for the "Invoice-sync" repository's main branch, then issues a normal access token in return. There's no secret and no certificate sitting anywhere waiting to be found, just a token that's only good for a few minutes and only good for this one job.
Permissions
Proving its identity only tells Microsoft Entra ID who "Invoice-sync" is. Permissions decide what it's allowed to do once that's settled, and this is where "Invoice-sync" story gets more common than anyone would like.
"Invoice-sync" only ever writes to one SharePoint list. But when it was set up, the fastest permission to find and get approved was Sites.ReadWrite.All, write access to every SharePoint site in the entire tenant. The narrower option, Sites.Selected, would have taken an extra step to scope to the one site "Invoice-sync" actually needed. That extra step didn't happen, so the job still only touches one list, but its credential is now worth far more to an attacker than the job itself would suggest.
This pattern repeats across almost every tenant, usually with the same handful of overly broad permissions:
| Common broad grant | What it actually allows | Narrower alternative |
|---|---|---|
| Application.ReadWrite.All | Create, modify, or delete any app registration or service principal in the tenant. | Scope the task narrowly, or move it to a purpose-built automation account instead of a general one. |
| Sites.ReadWrite.All | Read and write every SharePoint site in the tenant. | Sites.Selected, scoped to the specific site the job actually uses. |
| Mail.ReadWrite | Read and send mail as any mailbox in the organization. | Application Access Policies, which restrict the grant to a named set of mailboxes. |
Table 2 - Describes the different permission types across a tenant.
For an internal API rather than Microsoft Graph, the same idea applies through custom app roles: define a narrow role like Invoices.Write instead of a blanket admin role, and give "Invoice-sync" only that. A useful way to hold the whole idea in one line: how much damage a leak can do is roughly what the credential exposes, multiplied by what the permission allows. Fixing only one side of that still leaves the other open.
How It All Comes Together
None of these are separate boxes to check, they add up. The clearest way to see that is to compare two versions of "Invoice-sync": the one that likely exists in your tenant today, and the one it becomes after a deliberate cleanup.
| "Invoice-sync" as built | "Invoice-sync" after cleanup | |
|---|---|---|
| Credential | A client secret with no expiration set | Workload identity federation, nothing stored to leak |
| Permission | Sites.ReadWrite.All, granted once at setup | Sites.Selected, scoped to the one list it writes to |
| Ownership | No named owner; the engineer who set it up has moved teams | An owner is on record, and it's included in a recurring review |
| If it leaks | Immediate, tenant-wide SharePoint access | Nothing to leak, and even a forged token reaches one site |
Table 3 - Describes an example of before and after a lifecycle clean up.
Nothing in the cleaned-up version is strange. Federation, scoped permissions, and named ownership are all standard Microsoft Entra ID features, the difference is simply that none of the three was left at its easiest, least secure default.
Conclusion
Invoice-sync" was never the problem, nightly jobs like it exist in every tenant, and there's nothing wrong with needing one. The problem is treating its credential, its permission, and its owner as three separate afterthoughts instead of one decision made deliberately on the day it's created.