Back to blog
AzureBest PracticesCloud SecurityIdentity & AccessOperations & Compliance

Azure Key Vault Secret Has No Expiration: Why It Matters and How to Fix It

Learn why Azure Key Vault secrets without an expiration date are a security risk, and how to set expiry, automate rotation, and enforce it with policy.

TL;DR

This check flags Azure Key Vault secrets that have no expiration date set. Secrets that live forever are easy to forget and hard to rotate, which widens the blast radius of any leak. Fix it by setting an expiry date on each secret and enforcing rotation through automation.

Secrets stored in Azure Key Vault are usually the keys to your kingdom: database connection strings, API tokens, signing keys, and service credentials. When one of those secrets has no expiration date, it sits in the vault indefinitely. Nobody is forced to look at it, nobody rotates it, and if it ever leaks, an attacker can use it for as long as it stays valid, which could be years.

The keyvault_secretexpiry check catches exactly this situation. It looks at every secret in your Key Vaults and reports the ones missing an expires attribute.


What this check detects

Azure Key Vault secrets support an optional set of attributes that control their lifecycle, including nbf (not before), exp (expiration), and enabled. The exp attribute defines the point in time after which the secret is considered expired and will be rejected by the Key Vault service.

This check fails when a secret has no exp value configured. In the Key Vault data plane, that secret has no defined end of life. It will keep returning successfully on every GetSecret call until someone manually disables or deletes it.

Note: Setting an expiration date does not automatically rotate the secret or generate a new value. When a secret expires, Key Vault simply refuses to return its value to callers. You still need a process to create a new version before the old one expires, otherwise your application breaks.


Why it matters

A secret with no expiration is a silent liability. The risk is not that the secret is wrong today, it is that it stays valid forever with no forcing function to review it.

Leaked credentials stay useful indefinitely

Secrets leak in predictable ways: committed to a Git repo, pasted into a support ticket, dumped in a log file, or exposed through an SSRF vulnerability that hits the Key Vault data plane. When a leaked secret has an expiration, the window of usefulness for an attacker is bounded. When it has none, that window is open until a human notices something is wrong.

Rotation is the difference between a leaked credential being a brief incident and being a permanent backdoor.

Compliance frameworks expect rotation

PCI DSS, SOC 2, ISO 27001, and most internal security policies require periodic credential rotation. An auditor who finds production secrets that have never expired and were created three years ago will write that up. Expiration dates are the easiest way to prove a rotation lifecycle exists.

Stale secrets hide orphaned access

Long-lived secrets accumulate. A service principal credential created for a project that shipped two years ago may still be valid and still grant access to live resources. Without expiry, there is nothing prompting you to ask whether that access is still needed.

Warning: Adding an expiration date to a secret that an application is actively using will break that application the moment the secret expires if nothing rotates it first. Treat this as a process change, not a one-time field update.


How to fix it

The fix has two layers: set an expiration on existing secrets, and make sure a new version exists before the old one expires.

Find secrets without expiration

List the secrets in a vault and check their attributes with the Azure CLI:

az keyvault secret list \
  --vault-name myVault \
  --query "[?attributes.expires==null].{name:name, enabled:attributes.enabled}" \
  --output table

This returns every secret in myVault that has no expiration set.

Set an expiration on an existing secret

You set expiration on a specific secret version. To update the current version, use az keyvault secret set-attributes:

# Expire this secret 90 days from now (RFC3339 / UTC)
az keyvault secret set-attributes \
  --vault-name myVault \
  --name db-connection-string \
  --expires "$(date -u -d '+90 days' '+%Y-%m-%dT%H:%M:%SZ')"

Danger: Before setting an expiry on a production secret, confirm that something rotates it. If you set a 90 day expiry on a database password that nobody rotates, your application will start failing authentication exactly 90 days from now, likely at 2am.

Set expiration when creating a new secret

Always set --expires at creation time so secrets are never born immortal:

az keyvault secret set \
  --vault-name myVault \
  --name api-token \
  --value "s3cr3t-value" \
  --expires "$(date -u -d '+90 days' '+%Y-%m-%dT%H:%M:%SZ')"

Set expiration in Terraform

If you manage Key Vault secrets with Terraform, use the expiration_date argument. This makes expiry part of your reviewed, version-controlled infrastructure:

resource "azurerm_key_vault_secret" "api_token" {
  name            = "api-token"
  value           = var.api_token
  key_vault_id    = azurerm_key_vault.main.id
  expiration_date = timeadd(timestamp(), "2160h") # 90 days

  lifecycle {
    ignore_changes = [expiration_date]
  }
}

Note: Using timestamp() directly causes Terraform to want to update the resource on every plan, because the value changes each run. The ignore_changes block prevents that churn. For real rotation, drive the expiration from a managed rotation policy rather than recomputing it on apply.

Set expiration in Bicep / ARM

{
  "type": "Microsoft.KeyVault/vaults/secrets",
  "apiVersion": "2023-07-01",
  "name": "myVault/api-token",
  "properties": {
    "value": "[parameters('apiToken')]",
    "attributes": {
      "enabled": true,
      "exp": 1735689600
    }
  }
}

Note that ARM and the Key Vault REST API use Unix epoch seconds for the exp value, not an RFC3339 string.


Automate rotation so expiry does not break things

Expiration without rotation is a scheduled outage. The proper fix is to let Key Vault and your backing services rotate the secret automatically before it expires.

Use Key Vault rotation policies for supported secrets

For secrets that Key Vault can regenerate (for example, storage account keys integrated through managed storage, or secrets fed by an Event Grid driven rotation function), configure a rotation policy that rotates ahead of expiry:

az keyvault secret set-attributes \
  --vault-name myVault \
  --name storage-key \
  --expires "$(date -u -d '+90 days' '+%Y-%m-%dT%H:%M:%SZ')"

# Wire Key Vault "near expiry" events to a rotation Function via Event Grid
az eventgrid event-subscription create \
  --name secret-rotation \
  --source-resource-id "$(az keyvault show --name myVault --query id -o tsv)" \
  --endpoint-type azurefunction \
  --endpoint "/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/RotateSecret" \
  --included-event-types Microsoft.KeyVault.SecretNearExpiry

Key Vault emits a SecretNearExpiry event 30 days before expiration. Subscribe to it with an Azure Function that generates a new secret value, writes a new version into the vault, and updates the upstream system.

Tip: Microsoft publishes a ready-made secret rotation Function template that handles storage account keys, SQL credentials, and generic secrets. Start from that rather than building rotation logic from scratch, then point your applications at the latest secret version using the versionless secret URI so they pick up new values without a redeploy.


Prevent it from happening again

Catching missing expiry after the fact is fine, but the goal is to make immortal secrets impossible to create. Push the enforcement left.

Enforce with Azure Policy

Azure has a built-in policy that flags secrets exceeding a maximum validity period. Assign it in audit mode first, then tighten it:

# "Key Vault secrets should have an expiration date"
az policy assignment create \
  --name require-secret-expiry \
  --scope "/subscriptions/" \
  --policy "98728c90-32c7-4049-8429-847dc0f4fe37"

# "Secrets should have the specified maximum validity period"
az policy assignment create \
  --name secret-max-validity \
  --scope "/subscriptions/" \
  --policy "342e8053-e12e-4c44-be01-c3c2f318400f" \
  --params '{"maximumValidityInDays": {"value": 90}}'

These policies report non-compliant secrets in the Azure Policy compliance dashboard, giving you a single place to track drift across every vault in the subscription.

Gate IaC in CI/CD

Block the problem before it reaches Azure by scanning Terraform and Bicep in your pipeline. Tools like Checkov and tfsec flag azurerm_key_vault_secret resources missing expiration_date:

checkov -d ./infra --check CKV_AZURE_41

Fail the build on that finding so a pull request that adds a secret without expiry never merges.

Tip: Lensix runs keyvault_secretexpiry continuously across your subscriptions and surfaces every non-compliant secret with its vault and creation date, so you can prioritize the oldest, highest-risk secrets first instead of treating every finding equally.


Best practices

  • Set expiry at creation, always. Make --expires or expiration_date a required field in every script, module, and template that writes secrets.
  • Match expiry to risk. Highly sensitive secrets (signing keys, admin credentials) deserve shorter lifetimes than low-risk config values. 90 days is a reasonable default for most credentials.
  • Reference secrets by versionless URI. Point applications at https://myVault.vault.azure.net/secrets/api-token without a version so rotation does not require a redeploy.
  • Prefer managed identities over secrets entirely. The most secure secret is the one you never store. Where Azure resources support managed identities, use them and skip the secret lifecycle altogether.
  • Alert on near-expiry events. Subscribe to SecretNearExpiry and route it to your on-call channel so a missed rotation surfaces as a warning, not an outage.
  • Audit access alongside expiry. Enable Key Vault diagnostic logging so you can see who read a secret. Expiry limits the window, logging tells you what happened inside it.

Setting an expiration date is a small change, but it converts your secrets from permanent liabilities into managed, rotating credentials. Combined with rotation automation and policy enforcement, it closes one of the most common and most overlooked gaps in Azure secret management.

Fix Key Vault Secrets With No Expiration in Azure | Lensix