Skip to main content

Azure Key Vault module

Imports the certificate + key into an Azure Key Vault as a Key Vault certificate. Every renewal becomes a new version under the same certificate name, so Azure services that reference the certificate by its unversioned identifier (App Service, Application Gateway, API Management, AKS Secrets Store CSI) pick up the renewal on their own.

Overview​

  • Type: azurekeyvault, credential type: azure_service_principal (JSON {"tenant_id":"...","client_id":"...","client_secret":"..."}), slot default.
  • Per target: acquire an AAD token (client credentials) → GET the latest version of the KV certificate → compare the leaf's SHA-256 (over the DER) locally → identical and enabled → skip (idempotent); otherwise import an AES-256-encrypted PKCS#12 (leaf + chain + key, under a random per-import password sent in the request pwd field) as a new version under the same name.
  • The import is always PKCS#12 — Azure Key Vault does not accept EC private keys in PEM, and CertAutoPilot defaults to ECDSA P-256.
  • A certificate policy is set only on the first import: secret content type PKCS#12 plus key properties exportable: true (Application Gateway and other consumers that pull the certificate as a secret need an exportable key) and reuse_key: false (every renewal imports a fresh key pair). Renewals do not send a policy, so lifetime actions or an exportability change you make on the KV certificate afterwards are preserved; a name adopted from an existing object keeps its existing policy.
  • Every imported version is tagged certautopilot:certificate-id so ownership is tracked across renewals.
  • Rollback: supported — a rollback imports a previous retained certificate version into the Key Vault certificate object (as a new KV version under the same name, which unversioned consumer references pick up) (Rollback).
  • Sovereign clouds are supported: the vault URL suffix (.vault.azure.net, .vault.azure.cn, .vault.usgovcloudapi.net) selects the matching AAD authority automatically.

Requirements​

  • A service principal with access to the vault. Create one and grant it the RBAC role Key Vault Certificates Officer (or, on access-policy vaults, the certificate permissions Get + Import + List):
# 1. Create the service principal
az ad sp create-for-rbac --name certautopilot-kv

# 2. Grant it Key Vault Certificates Officer on the vault (RBAC vaults)
az role assignment create \
--role "Key Vault Certificates Officer" \
--assignee <appId-from-step-1> \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault-name>

# Access-policy vaults instead:
az keyvault set-policy --name <vault-name> \
--spn <appId-from-step-1> \
--certificate-permissions get import list
  • Key Vault firewall: if the vault restricts network access, allowlist CertAutoPilot's egress IP — a firewalled vault answers HTTP 403 and always surfaces as AZUREKV_AUTH (with a firewall hint in the message) even with a correct credential.
  • Outbound HTTPS (443) to both {vault}.vault.azure.net and login.microsoftonline.com (or the sovereign-cloud equivalents).

Target configuration​

FieldRequiredDescription
vault_urlyesThe vault URI, e.g. https://myvault.vault.azure.net. Sovereign clouds (.vault.azure.cn, .vault.usgovcloudapi.net) are supported. No port or path.
cert_namenoKV certificate name ([0-9a-zA-Z-]{1,127}). Empty → auto-generated cap-<token>-<slug>, unique per certificate.
tagsnoExtra tags on the imported certificate (max 10). The certautopilot: prefix is reserved for the ownership tag.
http_timeoutnoPer-request timeout in seconds. 0 = default (30); values below 5 are raised to 5; max 120.
{
"vault_url": "https://myvault.vault.azure.net",
"cert_name": "",
"tags": { "env": "prod" }
}

Choosing from the vault (live picker)​

The Certificate name field — on the target and in each certificate's override row — can list the vault's certificates instead of being typed. Each entry shows whether it is disabled, when it expires, and who owns it: an object CertAutoPilot already imports for this certificate, or one it manages for another certificate (adopting that name would make both certificates import versions under it on every renewal). Adopting a name you pick means the next deploy imports a new version under it.

The listing is read-only (GET /certificates, the same List permission the health check uses), returns object attributes and tags only — never the certificate or key material — and is capped at 500 names. Soft-deleted objects are not listed (Key Vault keeps them under a separate deleted-objects view) but their names stay reserved — typing one is refused with AZUREKV_CONFLICT, see Soft delete. A disabled object is listed and flagged: importing under its name adds a new, enabled version. Nothing is read until you open the list; the field always accepts a typed name. Listing through a saved target is a project operator action; browsing from an unsaved form, or after changing the vault URL on an existing target, needs project admin (or save first). The override drawer's Default (all targets) row stays manual-entry.

Identity and naming​

The deployment identity is the KV certificate name:

  • Empty cert_name (recommended) → an auto-generated, per-certificate name cap-<token>-<slug> — stable across renewals and unique per certificate, so one vault target serves many CertAutoPilot certificates without clobbering. Each certificate is tracked independently in per-cert remote state.
  • Explicit cert_name (target field or a per-distribution override) → the module adopts the existing KV certificate of that name and imports new versions under it.
  • If the name is already owned by a different CertAutoPilot certificate (its certautopilot:certificate-id ownership tag doesn't match), the run logs a warning but proceeds — deliberate adoption of a name is legitimate; the tag is rewritten to the new owner by the next real import (a run that skips because the leaf is already current writes no tags).

Renewals never create a second KV certificate: they add a version under the same name, which is exactly what unversioned Azure consumer references (e.g. an Application Gateway KV reference, polled roughly every 4 hours) expect.

Rollback​

Supported via previous-version re-deploy: a rollback imports a previous retained certificate version into the same KV certificate name — the module's normal deploy path, fed older material. Eligibility, the version picker, and auto-rollback: Rollback.

Key Vault's own versioning shapes what a rollback actually achieves here:

  • The rolled-back-to material lands as a new KV version, which becomes the current version of the certificate. The version that was current before the rollback is neither deleted nor disabled — Key Vault keeps it, and it remains directly addressable by its version id.
  • Consumers that reference the certificate by an unversioned URI (App Service, Application Gateway, APIM, the AKS CSI driver) pick the rolled-back material up on their own refresh schedule — Application Gateway polls roughly every 4 hours, for example. The rollback job reports success as soon as the import lands; the edge moves later.
  • Consumers pinned to a versioned URI never move at all. Repoint them manually — CertAutoPilot has no way to rewrite a pinned reference.

Plan rollback windows around the slowest consumer refresh, not around the job's completion time.

Soft delete and purge protection​

Azure Key Vault keeps deleted certificates in a soft-deleted state (typically 90 days). Importing over a soft-deleted name fails with a 409 ("...deleted but recoverable state... can only be recovered or purged"), surfaced as AZUREKV_CONFLICT. CertAutoPilot does not auto-recover. Either:

az keyvault certificate recover --vault-name <vault-name> --name <cert-name>
# or, if you truly want the name freed (blocked when purge protection is on):
az keyvault certificate purge --vault-name <vault-name> --name <cert-name>

…or pick a different cert_name (or leave it empty for the auto name).

Credential​

Create an Azure Service Principal credential (Settings → Distribution → Credentials):

{
"tenant_id": "00000000-0000-0000-0000-000000000000",
"client_id": "00000000-0000-0000-0000-000000000000",
"client_secret": "your-client-secret"
}

The principal needs Key Vault Certificates Officer (RBAC) or certificate permissions Get + Import + List (access policy) on the vault — see Requirements.

Verification​

The module's own certificate check (name present, latest version carries the expected thumbprint) is never invoked by the platform — post-deploy verification uses only the target's validation endpoints, which dial an Azure consumer over TLS. The health check acquires an AAD token, lists one certificate page (GET /certificates?maxresults=1) and then reads one certificate by name (GET /certificates/{name} on a name that is not expected to exist, where a 404 is the healthy answer). Both data-plane calls are checked because Key Vault will serve the list while refusing every per-object read — a vault whose Azure subscription has been disabled does exactly that — and the per-object read is the first thing a deployment performs. So a healthy result proves reachability, credential validity, and both the List and Get permissions a deployment actually needs. To verify a deployment:

az keyvault certificate show --vault-name <vault-name> --name <cert-name> \
--query "{thumbprint:x509ThumbprintHex, expires:attributes.expires, tags:tags}"

The expiry should match the renewed certificate, and tags should carry certautopilot:certificate-id.

Error codes​

CodeMeaningResolution
AZUREKV_CONNECTVault or AAD endpoint unreachable (temporary DNS failure, TCP, TLS, timeout), a vault 5xx, an unexpected redirect, or an HTTP 429/5xx from the Entra ID token endpointCheck outbound egress and the vault_url. Retried.
AZUREKV_AUTHToken rejected, missing permission, blocked by the vault firewall, or the owning Azure subscription is disabledThe message says which: fix tenant/client/secret; grant Key Vault Certificates Officer (or access-policy Get+Import+List); allowlist CertAutoPilot's egress IP in the Key Vault firewall; or re-enable the subscription in the Azure portal — a disabled subscription refuses per-object reads and writes no matter which role is granted.
AZUREKV_NOT_FOUNDVault or certificate not found, or the vault host does not resolve in DNS (a mistyped or deleted vault)Verify the vault URL (the message names the host that failed to resolve) and certificate name. Not retried.
AZUREKV_CONFLICTA soft-deleted certificate holds the name, or a certificate operation is pendingaz keyvault certificate recover (or purge — blocked by purge protection), or pick a different cert_name.
AZUREKV_VALIDATIONBad vault_url/cert_name, or Azure rejected the PFXCheck the field formats and the certificate material.
AZUREKV_QUOTAKey Vault throttling (429)Transient class: retried per target in a fan-out batch and by the sweep re-arm — the module itself performs no in-run backoff. Frequent throttling means lowering the module concurrency.
CREDENTIAL_MISSINGNo default credential was resolved for the target at run timeBind an azure_service_principal credential to the target. Not retried.

The live certificate picker reads at most 500 names, 22 pages or 25 seconds, whichever comes first.