7 min readMicrosoft Entra · App Registrations · Best Practices · Rotation
Rotating Entra client secrets without downtime: a runbook for app registrations
A runbook for rotating client secrets in Entra app registrations without downtime: add the new secret, switch consumers, verify, remove the old one. Steps in the portal, with Microsoft Graph PowerShell and Graph REST, plus common mistakes.
Creating a client secret takes two minutes in the portal. Outages come from the switch: somewhere the old value is still in use and nobody had it on the list. This post describes a procedure that works without interruption: portal, Microsoft Graph PowerShell and Graph REST.
Why rotation breaks integrations
Three properties make the swap risky:
- The value is shown exactly once. Once you leave the page, only the Secret ID and the first three characters remain. Without a copy, you create a new secret.
- Consumers cache the value. It sits in App Service settings, Key Vault, pipeline variables, a config file on an on-prem server, a Power Automate connector, each switched individually.
- Tokens already issued stay valid. An access token lives 60 to 90 minutes by default, so a deleted secret does not cut off running sessions immediately. The error shows up at the next token request, often an hour later and seemingly unrelated.
The third point is treacherous: deleting the old secret and watching for a minute proves nothing.
The overlap principle
An app registration can hold several client secrets and certificates at the same time, each valid on its own. That gives the only order that works without downtime:
- Add the new secret, keep the old one.
- Switch every consumer to the new secret.
- Verify that nothing authenticates with the old one anymore.
- Remove the old secret.
The gap between steps 2 and 4 is deliberate: at least a day, a week if weekly batch jobs are involved.
The runbook
- Check expiry date and owner. Which secret expires, who owns the app, is there documentation?
- Inventory the consumers. List every place that uses the value (see below).
- Add the new secret with description and lifetime. Put the value into the password manager or Key Vault immediately.
- Switch the consumers, one at a time. Restart services that read their configuration only at startup.
- Verify: successful sign-ins of the app in the sign-in logs, or a test token with the new value.
- Wait out the grace period, watching the sign-in logs for the old secret.
- Remove the old secret, identified by its Key ID.
- Document: date, Key ID, next expiry date, who rotated.
In the portal
In the Microsoft Entra admin center: App registrations, select the app, Certificates & secrets, tab Client secrets, New client secret. The dialog asks for:
- Description: see the naming convention below.
- Expires: 180 days (suggested), 365 days, 730 days or a custom date. 24 months is the maximum, Microsoft recommends less than 12.
After Add, the table shows Value and Secret ID. Copy Value. The Secret ID is the Key ID; it stays visible, the value does not. Removal later is the trash icon in the old secret's row.
With Microsoft Graph PowerShell
You need Application.ReadWrite.All or, for apps your identity owns, Application.ReadWrite.OwnedBy. $appObjectId is the Object ID of the app registration, not the Application (client) ID.
New secret with a twelve-month lifetime:
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$new = Add-MgApplicationPassword -ApplicationId $appObjectId -BodyParameter @{
passwordCredential = @{
displayName = "backup-sync / it-ops / 2026-09-28"
endDateTime = (Get-Date).AddMonths(12)
}
}
$new.SecretText # shown once
$new.KeyId # for the docs
List existing secrets and remove the old one by its Key ID:
(Get-MgApplication -ApplicationId $appObjectId -Property PasswordCredentials).PasswordCredentials |
Select-Object DisplayName, KeyId, StartDateTime, EndDateTime
Remove-MgApplicationPassword -ApplicationId $appObjectId -KeyId "f0b0b335-1d71-4883-8f98-567911bfdca6"
With Microsoft Graph REST
The same two actions as HTTP calls on the application resource. A PATCH on passwordCredentials is not supported, there are only the actions addPassword and removePassword.
POST https://graph.microsoft.com/v1.0/applications/{object-id}/addPassword
Content-Type: application/json
{
"passwordCredential": {
"displayName": "backup-sync / it-ops / 2026-09-28",
"endDateTime": "2027-09-28T00:00:00Z"
}
}
The response contains secretText (once), keyId and hint. Without endDateTime, Graph sets two years.
POST https://graph.microsoft.com/v1.0/applications/{object-id}/removePassword
Content-Type: application/json
{ "keyId": "f0b0b335-1d71-4883-8f98-567911bfdca6" }
Graph also accepts applications(appId='{client-id}') instead of the Object ID.
Finding every consumer of a secret
This is the step where rotations fail. Search:
- Key Vault: secrets whose name or tag references the app. If Key Vault is the distribution point, a new secret version is enough.
- App Service and Functions: application settings, connection strings, Key Vault references.
- Pipelines: variable groups in Azure DevOps, secrets in GitHub Actions, service connections.
- On-prem: config files, Windows services, scheduled tasks, third-party tools with their own credential store.
- Documentation: handover notes, the wiki, the ticket that set up the app.
The most reliable source is Entra itself: Entra ID, Monitoring & health, Sign-in logs, tab Service principal sign-ins lists every sign-in of the app with IP address and target resource. Any IP that keeps signing in after the switch, where nobody changed anything, is a forgotten consumer. The Application credential activity report under Usage & insights shows the last-used date per credential (Entra ID P1 or P2 required).
Naming convention for the description
The description is the only free-text field on a secret and should answer three questions: what for, who, since when. A pattern that works is consumer / team / yyyy-mm-dd, for example backup-sync / it-ops / 2026-09-28. With the date in place, it is obvious which of two parallel secrets is the new one. Keep the app's owners maintained anyway; a description does not replace a contact.
Verifying the cutover
Two ways:
- Sign-in logs: after restarting the consumer, a successful sign-in of the app appears; the entry details show the client credential type. No entry means the consumer still reads the old value, or none.
- Test token: a client credentials request against the token endpoint with the new value. An
access_tokenin the reply means the secret is active.AADSTS7000215(invalid client secret) usually means the Secret ID was copied instead of the value.
POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id={client-id}
&client_secret={new-value}
&scope=https://graph.microsoft.com/.default
&grant_type=client_credentials
Automating rotation
With more than a handful of apps, automate steps 3 to 7:
- Azure Automation runbook or Azure Function on a schedule: reads all secrets via Graph, adds a new one when less than 30 days remain and writes the value straight into Key Vault.
- Key Vault as the distribution point: consumers read the secret through a Key Vault reference or SDK instead of their own configuration. The switch becomes a new secret version, not a deployment.
- Event Grid: Key Vault emits
Microsoft.KeyVault.SecretNearExpiry30 days before expiry andSecretExpiredafterwards, see the Key Vault event schema. A Function as handler starts the rotation, provided the Key Vault secret carries the same expiry date as the Entra secret.
The automation needs its own credential with Application.ReadWrite.OwnedBy; a managed identity for the Function avoids the chicken-and-egg problem.
Certificates: same principle, different selector
Certificates follow the same procedure. Upload under Certificates & secrets, tab Certificates, Upload certificate, as .cer, .pem or .crt, public part only. In Graph the actions are addKey and removeKey; addKey requires proof of possession of an existing valid certificate, otherwise use the portal or update keyCredentials. The difference is on the consumer side: it selects the certificate by thumbprint or subject name from the certificate store. So the thumbprint in the configuration has to move along, or the client selects by subject and takes the newest. The private key belongs on the consumer or in Key Vault, never in Entra.
Common mistakes
| Mistake | Effect | Prevention |
|---|---|---|
| Old secret deleted first | Outage about an hour later, when running tokens expire | Always overlap: add, switch, verify, delete |
| Secret ID copied instead of Value | AADSTS7000215, the new secret appears "wrong" | Copy Value right after Add; Secret ID is documentation only |
| Rotated in only one of many tenants | The same integration fails for the other customers | Tick off each tenant on a list |
| Consumer not restarted | Old configuration stays in memory | Restart and check the sign-in |
Rotation starts with monitoring
The procedure above needs lead time: inventory, switch, grace period. Learning the expiry date on the day of the outage rules out overlap. Entra itself sends no warning, as described in Entra app registration secrets expire. A reminder 30 days before expiry is the minimum, 90 days for apps with many consumers.
How SecretExpiry helps
SecretExpiry reads the expiry dates of all client secrets and certificates across every connected tenant with the Application.Read.All permission. The service does not rotate anything itself, it reports. Notifications arrive by e-mail per event or as a weekly digest, via webhook in a Microsoft Teams or Slack channel, or as a calendar subscription (ICS). Each tenant can have its own notification address, and rotations already scheduled can be snoozed. The dashboard shows the urgent cases first. 14-day free trial, then from 10 € per tenant per month, details in the documentation.