Goal
A certificate stored in Key Vault is close to expiry, or has already expired. Two things can be wrong:
- Key Vault never made a new version. The policy can't auto-renew, or the request to the CA is stuck or failed.
- Key Vault has a new version, but the endpoint still serves the old one. The consumer is pinned to a version or hasn't synced yet.
Find out which one you have before you change anything. Check what the endpoint is actually serving:
echo | openssl s_client -connect www.contoso.com:443 -servername www.contoso.com 2>/dev/null \
| openssl x509 -noout -subject -enddate -fingerprint -sha1
Then compare the expiry date with Key Vault's current version (step 1). If they match, you have case 1. If Key Vault is newer, you have case 2.
Step 1: read the certificate and its policy
VAULT=kv-prod-certs; CERT=www-contoso-com
az keyvault certificate show --vault-name $VAULT -n $CERT \
--query "{version:id, expires:attributes.expires, enabled:attributes.enabled, thumbprint:x509ThumbprintHex, issuer:policy.issuerParameters.name, actions:policy.lifetimeActions}" -o jsonc
az keyvault certificate list-versions --vault-name $VAULT -n $CERT \
--query "sort_by([], &attributes.created)[].{created:attributes.created, expires:attributes.expires, enabled:attributes.enabled, thumb:x509ThumbprintHex}" -o table
Read issuer and actions together:
| Issuer name | Lifetime action | Will it renew itself? |
|---|---|---|
Self | AutoRenew | Yes, as a self-signed certificate |
| Configured issuer (DigiCert or GlobalSign integration) | AutoRenew | Yes, if the issuer account and domain validation are still valid |
| Configured issuer | EmailContacts | No. Key Vault only emails the certificate contacts |
Unknown (imported, or CSR signed by a non-integrated CA) | Any | No. Someone has to create a CSR, get it signed and merge it |
Most "auto-renew didn't work" tickets are an imported certificate (Unknown). Key Vault can't renew those.
Step 2: check for a pending or failed renewal
az keyvault certificate pending show --vault-name $VAULT -n $CERT \
--query "{status:status, detail:statusDetails, error:error, requestId:requestId, cancel:cancellationRequested}" -o jsonc
status / detail | Meaning | Action |
|---|---|---|
inProgress for hours or days | Waiting for the CA, often organisation or domain validation | Check the order in the CA portal and finish validation |
failed, error mentions authentication or account | Issuer credentials (API key, org ID) are wrong or have expired | Update the issuer (step 3) and renew again |
failed, error mentions domain or SAN | SAN not pre-validated on the CA account | Validate the domain with the CA, then renew |
inProgress, issuer Unknown | A manual CSR is waiting for a merge | Get it signed and merge it (step 4) |
No pending operation, Not found | Nothing was triggered | The lifetime action is missing or set to email only |
Step 3: fix the issuer and the policy
az keyvault certificate issuer list --vault-name $VAULT -o table
az keyvault certificate issuer show --vault-name $VAULT --issuer-name DigiCertIssuer
If the provider key has been rotated on the CA side, update the issuer:
az keyvault certificate issuer update --vault-name $VAULT --issuer-name DigiCertIssuer \
--account-id <account-id> --password <new-api-key> --organization-id <org-id>
Make sure the policy renews well before expiry, and that a person is still notified:
az keyvault certificate show --vault-name $VAULT -n $CERT --query policy > policy.json
# edit policy.json: set lifetimeActions to
# [ { "trigger": { "daysBeforeExpiry": 30 }, "action": { "actionType": "AutoRenew" } } ]
az keyvault certificate set-attributes --vault-name $VAULT -n $CERT --policy @policy.json
az keyvault certificate contact add --vault-name $VAULT --email platform-oncall@contoso.com
Tip
Use
daysBeforeExpiry(for example 30) rather than a highlifetimePercentage. As CAs move to shorter certificate lifetimes, a percentage trigger can leave you only a few days to fix a failed order.
Step 4: renew now
Integrated issuer or self-signed: create a new version using the policy you already have.
az keyvault certificate create --vault-name $VAULT -n $CERT --policy @policy.json
az keyvault certificate pending show --vault-name $VAULT -n $CERT --query status
Non-integrated CA (Unknown): creating a new version produces a CSR. Get it signed outside Key Vault, then merge the result:
az keyvault certificate create --vault-name $VAULT -n $CERT --policy @policy.json
az keyvault certificate pending show --vault-name $VAULT -n $CERT --query csr -o tsv > www.csr
# submit www.csr to the CA, download the signed certificate chain as PEM
az keyvault certificate pending merge --vault-name $VAULT -n $CERT --file signed-chain.pem
If you already have a signed PFX from the CA, import it as a new version of the same certificate name:
az keyvault certificate import --vault-name $VAULT -n $CERT -f www-2027.pfx --password '<pfx-password>'
Warning
Always renew into the existing certificate name. A new name means every consumer has to be repointed by hand. Don't disable or delete the old version until every endpoint serves the new thumbprint. Consumers that are pinned to that version break straight away.
Step 5: make each consumer pick up the new version
Consumers only rotate on their own if they reference the versionless secret or certificate ID. Sync intervals are set by each service and can change, so check the current docs before you plan around them.
| Consumer | How it rotates | Check / force |
|---|---|---|
| Application Gateway v2 | Polls Key Vault (documented at about every 4 hours) when keyVaultSecretId has no version | Remove the version from the ID; any gateway update triggers a fetch |
| App Service (Key Vault certificate) | Syncs automatically on a schedule (documented as within 24 hours) | Portal Certificates > Sync, or re-import |
| Front Door Standard/Premium | Rotates if the secret version is set to Latest | Set Latest on the secret; check the timing in the current docs |
| API Management | Polls versionless Key Vault references | Check Custom domains, re-save to force a fetch |
| VMs | Key Vault VM extension polling | Check the extension's pollingIntervalInS and the observed certificates list |
Application Gateway: check and fix the reference.
az network application-gateway ssl-cert list -g rg-edge --gateway-name agw-prod \
--query "[].{name:name, kv:keyVaultSecretId}" -o table
az network application-gateway ssl-cert update -g rg-edge --gateway-name agw-prod -n www-cert \
--key-vault-secret-id "https://$VAULT.vault.azure.net/secrets/$CERT"
The gateway's managed identity needs Key Vault Secrets User on the vault (or get on secrets with access policies). It also needs a network path to the vault if the vault firewall is on. If it can't reach Key Vault, it keeps serving the last certificate it fetched until that certificate expires.
App Service: compare thumbprints and re-import if needed.
az webapp config ssl list -g rg-web --query "[].{name:name, thumb:thumbprint, expires:expirationDate, kv:keyVaultSecretStatus}" -o table
az webapp config ssl import -g rg-web -n app-www \
--key-vault $VAULT --key-vault-certificate-name $CERT
If the thumbprint changes on import, update the TLS binding with az webapp config ssl bind --certificate-thumbprint <new> --ssl-type SNI -g rg-web -n app-www.
Run the openssl s_client check again on every hostname. The work is done only when every endpoint shows the new expiry date.
Prevent it next time
Send Key Vault certificate events to on-call. CertificateNearExpiry fires before expiry, and CertificateNewVersionCreated confirms that renewal worked:
az eventgrid system-topic create -g rg-sec -n kv-prod-certs-topic --location westeurope \
--topic-type Microsoft.KeyVault.vaults \
--source $(az keyvault show -n $VAULT --query id -o tsv)
az eventgrid system-topic event-subscription create -g rg-sec --system-topic-name kv-prod-certs-topic \
-n cert-expiry-to-oncall --endpoint-type webhook --endpoint https://alerts.contoso.com/kv \
--included-event-types Microsoft.KeyVault.CertificateNearExpiry Microsoft.KeyVault.CertificateExpired Microsoft.KeyVault.CertificateNewVersionCreated
- Run a weekly report of every certificate across your vaults that expires within 45 days:
for v in $(az keyvault list --query "[].name" -o tsv); do
az keyvault certificate list --vault-name "$v" \
--query "[?attributes.expires < '$(date -u -d '+45 days' +%Y-%m-%dT%H:%M:%S)'].{vault:'$v', name:name, expires:attributes.expires}" -o tsv
done
- Treat every
Unknown-issuer certificate as a manual renewal with a named owner and a calendar reminder, or move it to an integrated issuer. - Use versionless IDs everywhere. Pinned versions are the most common reason a renewal "worked" but the site still broke.
- Synthetic-test the served certificate's expiry date on each public hostname. Don't rely only on what Key Vault reports.