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":"..."}), slotdefault. - Per target: acquire an AAD token (client credentials) →
GETthe 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 requestpwdfield) 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) andreuse_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-idso 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.netandlogin.microsoftonline.com(or the sovereign-cloud equivalents).
Target configuration
| Field | Required | Description |
|---|---|---|
vault_url | yes | The vault URI, e.g. https://myvault.vault.azure.net. Sovereign clouds (.vault.azure.cn, .vault.usgovcloudapi.net) are supported. No port or path. |
cert_name | no | KV certificate name ([0-9a-zA-Z-]{1,127}). Empty → auto-generated cap-<token>-<slug>, unique per certificate. |
tags | no | Extra tags on the imported certificate (max 10). The certautopilot: prefix is reserved for the ownership tag. |
http_timeout | no | Per-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 namecap-<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-idownership 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
| Code | Meaning | Resolution |
|---|---|---|
AZUREKV_CONNECT | Vault 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 endpoint | Check outbound egress and the vault_url. Retried. |
AZUREKV_AUTH | Token rejected, missing permission, blocked by the vault firewall, or the owning Azure subscription is disabled | The 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_FOUND | Vault 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_CONFLICT | A soft-deleted certificate holds the name, or a certificate operation is pending | az keyvault certificate recover (or purge — blocked by purge protection), or pick a different cert_name. |
AZUREKV_VALIDATION | Bad vault_url/cert_name, or Azure rejected the PFX | Check the field formats and the certificate material. |
AZUREKV_QUOTA | Key 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_MISSING | No default credential was resolved for the target at run time | Bind 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.