Every client secret on an Entra ID app registration is a long-lived password that can be copied, leaked or stolen from whoever holds it, and most of them exist only because the workload was set up before federation was an option. Workload identity federation lets the app trust a token issued by the platform the workload already runs on (GitHub Actions, Kubernetes, Google Cloud, AWS, or an Azure user-assigned managed identity) and exchange it for an Entra access token, so no secret is stored anywhere. This guide is the migration job: finding which secrets can be replaced, adding a federated identity credential alongside the secret, cutting the workload over, proving in the logs that the secret is no longer used, and then removing it and blocking new ones. It is written for Entra administrators, platform engineers and NHI programme owners. For the wider risks of client secrets see the Azure Entra Client Secrets Security Guide; for the concept across clouds see the Workload Identity Federation Guide.
Key takeaways
- Federation removes the secret, not the app. The app registration, its permissions and its service principal stay the same; only the credential type changes, which is why the cutover can be done with overlap and no permission change.
- Three values must match exactly. The federated identity credential's issuer, subject and audience must match the incoming token's iss, sub and aud claims; a wrong subject is accepted at creation and fails silently at exchange.
- Prove the secret is unused before removing it. The ClientCredentialType, FederatedCredentialId and ServicePrincipalCredentialKeyId columns in AADServicePrincipalSignInLogs show which credential each sign-in used.
- The subject is your new security boundary. A subject scoped to a GitHub environment or a single Kubernetes service account is tight; a subject that any branch or namespace can satisfy hands the app's permissions to whoever can run code there.
- Finish by blocking secrets. App management policies with passwordAddition stop the secret coming back next time someone is in a hurry.
When you need this
- CI/CD pipelines that deploy to Azure with a client secret stored as a pipeline variable (GitHub Actions, GitLab CI, Azure Pipelines service connections). For GitLab the end-to-end setup is in How to set up workload identity federation for GitLab CI to Azure.
- Kubernetes workloads on AKS, EKS, GKE or on-premises clusters that mount a secret to call Microsoft Graph, Key Vault or Azure Resource Manager.
- Workloads in another cloud (Google Cloud service accounts, AWS through IAM outbound identity federation) that call Entra-protected APIs.
- Azure-hosted apps that use an app registration rather than a managed identity, typically because the app is multitenant or needs app permissions in another tenant. The app can trust a user-assigned managed identity instead of a secret.
- After an incident or supplier compromise, when rotating secrets would only restart the clock. In the Commvault Metallic breach the exposed credentials were client secrets for Entra app registrations held by a vendor; a credential that is never stored cannot be taken from someone else's environment.
Federation does not fit everything. Workloads on platforms that cannot issue an OIDC token, and third-party SaaS that only accepts a client ID and secret, stay on secrets or certificates. Move those to certificates with short lifetimes and track them with How to find expiring app secrets and certificates in Entra ID.
Before you start
| Requirement | Detail |
|---|---|
| Read-only discovery | Graph scope Application.Read.All; Log Analytics read access to the workspace receiving Entra sign-in logs (AADServicePrincipalSignInLogs) |
| Adding a federated credential | Anyone who can add a secret or certificate to the app can add a federated identity credential. Roles that include microsoft.directory/applications/credentials/update: Application Administrator, Application Developer, Cloud Application Administrator, or the app owner |
| Blocking secrets afterwards | Global Administrator, or Security Administrator plus Cloud Application Administrator or Application Administrator, for app management policies |
| Tooling | Azure CLI (az ad app federated-credential), Az.Resources prerelease module (New-AzADAppFederatedCredential), or Graph /applications/{objectId}/federatedIdentityCredentials |
| Workload side | An Azure Identity SDK or MSAL version that supports client assertions; for AKS, Azure Identity minimums are .NET 1.9.0, Go 1.3.0, Java 1.9.0, Node.js 3.2.0 and Python 1.13.0 |
| Change record | App ID, secret keyId being replaced, consuming workload, owner, and the exact issuer and subject planned |
The issuer, subject and audience for each source platform, as Microsoft documents them:
| Workload runs on | Issuer | Subject format | Audience |
|---|---|---|---|
| GitHub Actions | https://token.actions.githubusercontent.com | repo:<org/repo>:environment:<name>, repo:<org/repo>:ref:refs/heads/<branch>, repo:<org/repo>:ref:refs/tags/<tag> or repo:<org/repo>:pull-request | api://AzureADTokenExchange |
| Kubernetes (AKS, EKS, GKE, self-managed) | The cluster's OIDC issuer URL | system:serviceaccount:<namespace>:<service account> | api://AzureADTokenExchange |
| Google Cloud | https://accounts.google.com | The service account's unique (numeric) ID | api://AzureADTokenExchange |
| Azure user-assigned managed identity | https://login.microsoftonline.com/{tenant}/v2.0 | The managed identity's object (principal) ID | api://AzureADTokenExchange (US Government: api://AzureADTokenExchangeUSGov; China: api://AzureADTokenExchangeChina) |
Limits to plan around: 20 federated identity credentials per app or user-assigned managed identity; issuer, subject and audience up to 600 characters each; exactly one audience; no wildcards in any property; the issuer and subject pair must be unique on the app; the credential name (3 to 120 characters, alphanumeric, dash or underscore) cannot be changed later. Only RS256-signed issuers are tested, and Entra ID issued tokens cannot be used as the external token, except through the managed identity scenario above.
Step-by-step
Steps 1 to 3 are read-only. Steps 4 to 6 add a credential and change the workload, and are reversible. Step 7 is destructive.
- Inventory secrets and the workloads using them. Export passwordCredentials from every app registration and service principal (the method is in How to find expiring app secrets and certificates in Entra ID). Then find where each secret is used:
The IP addresses, user agents and resource names tell you whether the caller is a GitHub-hosted runner, a cluster, a cloud VM or a person's laptop. Unused secrets are removal candidates, not migration candidates.AADServicePrincipalSignInLogs | where TimeGenerated > ago(30d) | where ResultType == "0" | summarize SignIns=count(), IPs=make_set(IPAddress, 20), LastSeen=max(TimeGenerated) by AppId, ServicePrincipalName, ClientCredentialType, ServicePrincipalCredentialKeyId | order by SignIns desc - Classify each secret by target platform. Use the decision table below. Record the issuer and the narrowest subject the platform can produce for that one workload.
- Check the issuer. Confirm the platform's OIDC discovery document is reachable from the internet (Entra validates the token against the issuer's keys) and that the issuer string in a real token matches what you will enter. GitHub issuers appear in Microsoft's examples both with and without a trailing slash: use the value in the token. A leading or trailing space in the configured issuer blocks the exchange.
- Add the federated identity credential (additive). Create it on the same app registration, leaving the secret in place:
{ "name": "gh-payments-prod", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:contoso/payments:environment:production", "description": "Replaces secret keyId 1a2b... CHG-1234", "audiences": ["api://AzureADTokenExchange"] }
Put the change number and the replaced keyId in the description so the link survives staff changes. Wait several minutes before testing: replication across a region takes time, and early requests fail with AADSTS70021 (no matching federated identity record found).az ad app federated-credential create --id <appId> --parameters fic.json az ad app federated-credential list --id <appId> - Change the workload to present a token instead of the secret. The token request swaps client_secret for client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and a client_assertion that the platform issued, sent to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token with grant_type=client_credentials. In practice:
- GitHub Actions: grant the job id-token: write, and pass client-id, tenant-id and subscription-id to the Azure login action without a client secret. Hardening of the GitHub side is in the GitHub Actions OIDC Federation Hardening Guide.
- AKS: annotate the service account with azure.workload.identity/client-id, label the pod azure.workload.identity/use: "true", and let the SDK read AZURE_FEDERATED_TOKEN_FILE. Do not hard-code the token path.
- Other Kubernetes or clouds: project a service account token (or fetch a Google or AWS identity token) and pass it to ClientAssertionCredential or the MSAL WithClientAssertion option.
- Azure compute using an app registration: acquire a managed identity token for api://AzureADTokenExchange/.default and use it as the client assertion; Microsoft.Identity.Web supports this with SourceType SignedAssertionFromManagedIdentity. Only user-assigned managed identities can be used.
- Run both in parallel and watch the logs. Deploy the federated workload, keep the secret valid, and confirm sign-ins now carry a FederatedCredentialId and no ServicePrincipalCredentialKeyId for the old secret:
Leave the secret in place for at least one full run cycle of the workload: monthly jobs, failover paths and disaster recovery runbooks often use a copy of the secret nobody remembers.AADServicePrincipalSignInLogs | where TimeGenerated > ago(7d) and AppId == "<appId>" | summarize count(), LastSeen=max(TimeGenerated) by ClientCredentialType, ServicePrincipalCredentialKeyId, FederatedCredentialId - Remove the secret (destructive). When the old keyId has had no successful sign-ins for the agreed window, remove it in Certificates & secrets or with the Graph removePassword action, then delete the copy from the pipeline variable store or vault. Removal is immediate and cannot be undone; any caller still using it fails.
- Block new secrets on the app. Apply an app management policy with passwordAddition (and symmetricKeyAddition) restricted, scoped to migrated apps or to all apps created after a chosen date. See Hardening afterwards.
Choosing the replacement
| Situation | Best replacement | Why, and the trade-off |
|---|---|---|
| Workload runs in Azure, single tenant, calls Azure resources | Managed identity directly, no app registration | No credential to manage at all; the app registration may be retired entirely |
| Workload runs in Azure but needs an app registration (multitenant, cross-tenant app permissions) | Federated credential trusting a user-assigned managed identity | Keeps the app's identity and consent; cross-tenant works, cross-cloud does not |
| GitHub Actions, GitLab CI, Azure Pipelines | Federated credential with environment-scoped subject | Subject per environment gives a review gate; branch subjects have no pattern matching, so one credential per branch counts against the 20 limit |
| Kubernetes inside or outside Azure | Federated credential per service account | Namespace and service account become the boundary; anyone who can create pods with that service account gets the token |
| Google Cloud or AWS workloads | Federated credential using the cloud's identity token | No secret crosses clouds; depends on the other cloud's IAM being well governed. See the Cross-cloud workload identity federation guide |
| Platform cannot issue OIDC tokens, or a SaaS supplier needs a credential | Certificate with short lifetime, or secret with capped lifetime | Still a stored credential; monitor it and revisit when the platform adds federation |
Small organisations: start with CI/CD, where a handful of federated credentials remove the most exposed secrets in an afternoon. Large organisations: the 20-credential limit and the one-subject-per-credential rule push towards one app registration per workload or environment rather than one shared deployment app, which is also better for least privilege. Flexible federated identity credentials, which match claims by expression rather than an exact subject, are in preview at the time of writing; treat any pattern-based subject as a widening of trust that needs review.
Verify it worked
- Sign-in logs: every successful sign-in for the app over the observation window shows a FederatedCredentialId and no ServicePrincipalCredentialKeyId matching a password credential.
- Credential export: the app's passwordCredentials collection is empty; federatedIdentityCredentials lists only the expected issuer and subject pairs.
- Stores cleaned: the secret has been deleted from pipeline variables, Kubernetes Secrets, Key Vault and any runbook that held it.
- Negative test: a run from a branch or namespace outside the subject is refused. An exchange with a non-matching subject fails without a descriptive error, so test the refusal deliberately rather than assuming it.
Rollback and common errors
- AADSTS70021 straight after creation: replication has not finished. Wait and retry before changing anything.
- AADSTS70021 that does not clear: issuer, subject or audience mismatch. Decode a real token and compare iss, sub and aud character by character, including the trailing slash and case.
- Exchange fails after the cutover: while the secret still exists, roll the workload configuration back to it; the federated credential can stay. Once the secret is removed there is no rollback: add a new short-lived secret only as an emergency measure and record it.
- 409 Conflict in infrastructure as code: credentials on the same user-assigned managed identity were created in parallel. Create them serially (ARM "mode": "serial" or dependsOn chains; Terraform AzureRM 3.40.0 or later does this).
- 400 "Issuer and subject combination already exists": list existing credentials; an earlier attempt already created it.
Hardening afterwards
- Block secrets: app management policies support passwordAddition, customPasswordAddition and symmetricKeyAddition to block secrets, and passwordLifetime and asymmetricKeyLifetime to cap the ones you keep. Roll out with exclusions for apps not yet migrated.
- Control who can add federated credentials: anyone who can add a credential to an app can add a federated one pointing at an issuer they control. Restrict app ownership and Application Administrator assignments, and for user-assigned managed identities consider the Azure Policy deny rule on Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials outside approved deployment paths.
- Alert on new credentials: a new federated identity credential, secret or certificate on an existing app is a persistence technique (ATT&CK T1098.001 Additional Cloud Credentials). Monitor the Entra audit log. See the Entra ID Sign-In and Audit Log Detection Guide.
- Review permissions while you are there: federation changes how the app authenticates, not what it can do. Use the migration ticket to remove unused Graph application permissions.
- Apply Conditional Access for workload identities where licensed, so a token from an unexpected location is refused even if the subject matches.
Common failure patterns
- Broad subjects: trusting a pull-request subject or a whole repository's branches on an app with write permissions lets untrusted code obtain production tokens. Use environments with required reviewers.
- Leaving the secret "just in case": the migration adds a federated credential and never removes the secret, so the attack surface grows rather than shrinks.
- Shared deployment apps: one app registration with many federated credentials for many pipelines hits the 20-credential limit and gives every pipeline the union of permissions.
- Removing the secret before the last consumer is found: a quarterly batch job or a DR script fails weeks later. The sign-in log window must cover the workload's longest cycle.
- Unmanaged issuers: trusting a self-managed cluster's issuer whose signing keys sit on a node anyone can reach turns the cluster admin into an Entra app owner.
Detection and measurement
- Percentage of app registrations with at least one password credential (target: falling each quarter).
- Share of service principal sign-ins by ClientCredentialType, tracked monthly.
- Migrated apps with a secret still present (target zero after the observation window).
- New federated identity credentials added outside the approved pipeline (target zero; each one investigated).
- Federated credentials whose subject is a pull-request or branch pattern on apps with write permissions.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev 5 | IA-5 Authenticator Management; IA-9 Service Identification and Authentication | Replacing stored authenticators with platform-issued, short-lived tokens |
| NIST SP 800-53 Rev 5 | AC-6 Least Privilege | Narrow subjects and per-workload apps |
| NIST CSF 2.0 | PR.AA-01; PR.AA-05 | Service credentials managed; access permissions defined and reviewed |
| MITRE ATT&CK v20 | T1552.001 Credentials In Files; T1098.001 Additional Cloud Credentials | Secrets in pipeline files and config removed; new credentials monitored |
Practitioner checklist
- Export every client secret and map it to its caller from AADServicePrincipalSignInLogs.
- Remove unused secrets instead of migrating them.
- Pick the narrowest subject the platform supports, preferably an environment or a single service account.
- Add the federated credential alongside the secret, with the change number in its description.
- Switch the workload to client assertions and add token retry logic.
- Confirm FederatedCredentialId on all sign-ins for a full workload cycle before removing the secret.
- Remove the secret from Entra and from every store that held a copy.
- Block new secrets with an app management policy and alert on new credentials of any type.
Standards and references
- Microsoft Learn: Workload identity federation
- Microsoft Learn: Federated identity credential considerations and restrictions
- Microsoft Learn: Configure an app to trust an external identity provider
- Microsoft Learn: Configure an application to trust a managed identity
- Microsoft Learn: OAuth 2.0 client credentials flow
- Microsoft Learn: AADServicePrincipalSignInLogs table reference
- Microsoft Learn: Configure app management policies
- Microsoft Learn: Use Microsoft Entra Workload ID with AKS
Related NHI Mgmt Group resources: Azure Entra Client Secrets Security Guide · Workload Identity Federation Guide · Multi-Cloud Workload Identity Troubleshooting Guide · Azure Managed Identities Security Guide