Home / Azure

AzureRunbookIntermediate

Expiring Entra ID app registration secrets and certificates, finding and rotating them without an outage

Find app registration secrets and certificates that expire soon, add the new credential alongside the old one, cut every consumer over, prove the old key is unused, then remove it.

Deepak Bhardwaj7 min read

Applies toMicrosoft Entra ID app registrations, Azure CLI 2.x, Microsoft Graph PowerShell SDK, Azure Key Vault

An app registration's client secret expires and the integration that uses it stops working: pipelines fail, an API returns 401, a background job stops processing. The sign-in error is usually one of these:

Error Meaning Fix
AADSTS7000222 The client secret has expired Add a new secret and update the consumer (this runbook)
AADSTS7000215 Invalid client secret The consumer has the wrong value, often the secret ID instead of the secret value
AADSTS700027 Client assertion failed signature validation The certificate the app signs with is not (or no longer) on the app registration
AADSTS700016 Application not found in the directory Wrong client ID or wrong tenant, not an expiry problem

The safe pattern is always add, cut over, verify, remove. Never replace the old credential in one step.

1. Find what expires soon

Microsoft Graph PowerShell lists secrets and certificates across all app registrations:

Connect-MgGraph -Scopes "Application.Read.All"
$cutoff = (Get-Date).AddDays(45)

Get-MgApplication -All -Property Id,AppId,DisplayName,PasswordCredentials,KeyCredentials |
  ForEach-Object {
    $app = $_
    foreach ($c in $app.PasswordCredentials) {
      [pscustomobject]@{ App=$app.DisplayName; AppId=$app.AppId; Type='Secret';
        Name=$c.DisplayName; KeyId=$c.KeyId; Expires=$c.EndDateTime }
    }
    foreach ($c in $app.KeyCredentials) {
      [pscustomobject]@{ App=$app.DisplayName; AppId=$app.AppId; Type='Certificate';
        Name=$c.DisplayName; KeyId=$c.KeyId; Expires=$c.EndDateTime }
    }
  } |
  Where-Object { $_.Expires -lt $cutoff } |
  Sort-Object Expires | Format-Table -AutoSize

For a single app with the Azure CLI:

APP_ID=00000000-0000-0000-0000-000000000000
az ad app credential list --id $APP_ID --query "[].{name:displayName, keyId:keyId, expires:endDateTime}" -o table
az ad app credential list --id $APP_ID --cert --query "[].{name:displayName, keyId:keyId, expires:endDateTime}" -o table

Rows with an Expires date in the past are already broken or unused. Rows in the next few weeks are your work list.

2. Find every consumer before you touch anything

The secret value is only shown once, so Entra ID cannot tell you where it has been pasted. Check the usual places:

Where the secret lives How to update it
Key Vault secret read by the app New secret version (step 4)
App Service / Functions app setting az webapp config appsettings set or a Key Vault reference
Azure DevOps service connection Edit the service connection and paste the new value
GitHub Actions secret Repository or environment secret
AKS cluster using a service principal (older clusters) az aks update-credentials --reset-service-principal
On-premises scripts, scheduled tasks, third-party SaaS Owner of that system

Use the sign-in logs to find out what is actually authenticating with the app. With service principal sign-in logs sent to Log Analytics:

AADServicePrincipalSignInLogs
| where TimeGenerated > ago(14d)
| where AppId == "00000000-0000-0000-0000-000000000000"
| summarize signIns = count(), lastSeen = max(TimeGenerated)
    by ServicePrincipalCredentialKeyId, IPAddress, ResourceDisplayName
| order by lastSeen desc

ServicePrincipalCredentialKeyId matches the KeyId from step 1, so you can see which credential each caller uses and from which IP. In the portal, the same data is under Entra ID > Sign-in logs > Service principal sign-ins.

3. Add the new credential next to the old one

Caution

az ad app credential reset without --append deletes every existing secret and certificate on the app and creates one new secret. Every consumer breaks at once. Always pass --append when you rotate.

az ad app credential reset --id $APP_ID --append \
  --display-name "prod-2026-10" --end-date 2027-04-24 \
  --query password -o tsv

The Graph PowerShell equivalent:

$app = Get-MgApplication -Filter "appId eq '$appId'"
$new = Add-MgApplicationPassword -ApplicationId $app.Id -PasswordCredential @{
  displayName = 'prod-2026-10'
  endDateTime = (Get-Date).AddMonths(6)
}
$new.SecretText   # shown once, store it immediately
$new.KeyId

Put the date or ticket number in the display name. When there are three secrets on an app, the name is the only way to tell which is which.

For a certificate credential, upload only the public part and append it:

az ad app credential reset --id $APP_ID --append --cert "@./app-prod-2026.pem"

Your tenant may limit secret lifetimes through an app management policy. If the command fails with a policy error, use a shorter --end-date or a certificate.

4. Cut consumers over

Store the new value in Key Vault rather than in each app's config:

az keyvault secret set --vault-name kv-prod --name app-erp-client-secret \
  --value "$NEW_SECRET" --expires 2027-04-24T00:00:00Z
  • Versionless Key Vault references in App Service and Functions (@Microsoft.KeyVault(VaultName=kv-prod;SecretName=app-erp-client-secret)) pick up the new version automatically, but not instantly. A configuration change on the app forces an immediate re-fetch; a restart is the simplest way to be certain.
  • Apps that cache the secret at start-up need a restart or redeploy.
  • Pipelines and SaaS tools need the value pasted in by whoever owns them. Ask them to confirm with a test run, not just "done".

Test each consumer after it changes, while the old secret still works. Problems then stay limited to one consumer.

5. Prove the old credential is unused, then remove it

Re-run the sign-in query from step 2 and filter on the old key ID:

AADServicePrincipalSignInLogs
| where TimeGenerated > ago(2d)
| where ServicePrincipalCredentialKeyId == "<old-key-id>"
| summarize count(), lastSeen = max(TimeGenerated) by IPAddress, ResourceDisplayName

No rows over a period that includes your slowest consumer (a weekly job needs at least a week) means it is safe to remove the old key:

az ad app credential delete --id $APP_ID --key-id <old-key-id>
# certificate credential:
az ad app credential delete --id $APP_ID --key-id <old-key-id> --cert
Remove-MgApplicationPassword -ApplicationId $app.Id -KeyId '<old-key-id>'

Warning

A deleted credential cannot be restored. If something still uses it, the only way back is to issue a new secret and update that consumer. When in doubt, let the old secret expire on its own date instead of deleting it early.

6. If it has already expired

You are in an outage, so shorten the process but keep the order:

  1. Add a new secret with --append (step 3). The expired one does no harm, so leave it.
  2. Update the consumer that is failing first, then the others found in step 2.
  3. Confirm sign-ins succeed in the service principal sign-in logs.
  4. Delete the expired credential afterwards to keep the app tidy.

Prevent it next time

  • Remove secrets where you can. Azure-hosted workloads should use managed identities. GitHub Actions, Azure DevOps and Kubernetes can use workload identity federation (az ad app federated-credential create), which needs no secret at all.
  • Prefer certificates stored in Key Vault over secrets when a credential is unavoidable. Key Vault can renew them and alert before expiry.
  • Schedule the step 1 report in an Azure Automation runbook or pipeline, and send anything under 45 days to the owning team's queue.
  • Record an owner for every app registration (the Owners blade) so the report has someone to send the ticket to.
  • Keep lifetimes short and consistent, such as six months, so rotation becomes a routine task rather than a surprise once every two years.