All posts

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.

Read this post in German

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:

  1. Add the new secret, keep the old one.
  2. Switch every consumer to the new secret.
  3. Verify that nothing authenticates with the old one anymore.
  4. 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

  1. Check expiry date and owner. Which secret expires, who owns the app, is there documentation?
  2. Inventory the consumers. List every place that uses the value (see below).
  3. Add the new secret with description and lifetime. Put the value into the password manager or Key Vault immediately.
  4. Switch the consumers, one at a time. Restart services that read their configuration only at startup.
  5. Verify: successful sign-ins of the app in the sign-in logs, or a test token with the new value.
  6. Wait out the grace period, watching the sign-in logs for the old secret.
  7. Remove the old secret, identified by its Key ID.
  8. 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_token in 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.SecretNearExpiry 30 days before expiry and SecretExpired afterwards, 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

MistakeEffectPrevention
Old secret deleted firstOutage about an hour later, when running tokens expireAlways overlap: add, switch, verify, delete
Secret ID copied instead of ValueAADSTS7000215, the new secret appears "wrong"Copy Value right after Add; Secret ID is documentation only
Rotated in only one of many tenantsThe same integration fails for the other customersTick off each tenant on a list
Consumer not restartedOld configuration stays in memoryRestart 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.