Home / Azure

AzureRunbookAdvanced

Key Vault certificate didn't auto-renew, or renewed but apps still serve the old one: a runbook

Find out why a Key Vault certificate didn't renew (issuer, lifetime action, pending request), renew it safely, and make App Gateway, App Service and Front Door pick up the new version.

Deepak Bhardwaj8 min read

Applies toAzure Key Vault (Standard/Premium) certificates, Application Gateway v2, App Service, Front Door Standard/Premium, API Management, Azure CLI 2.60+

Goal

A certificate stored in Key Vault is close to expiry, or has already expired. Two things can be wrong:

  1. Key Vault never made a new version. The policy can't auto-renew, or the request to the CA is stuck or failed.
  2. 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 high lifetimePercentage. 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.