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 resetwithout--appenddeletes every existing secret and certificate on the app and creates one new secret. Every consumer breaks at once. Always pass--appendwhen 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:
- Add a new secret with
--append(step 3). The expired one does no harm, so leave it. - Update the consumer that is failing first, then the others found in step 2.
- Confirm sign-ins succeed in the service principal sign-in logs.
- 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.