How To: Get a TLS Certificate onto an Azure Application Gateway / WAF
You are deploying an Azure Application Gateway v2 (WAF_v2) with Bicep/Terraform and its HTTPS
listener needs a certificate. This guide covers what the IaC actually automates, what stays a
manual (or separately-automated) step, and how it maps onto strata’s own model — see the
worked example in config/: resources/networking.yaml
(key-vault, app-gateway-waf), integrations/azure-keyvault.yaml, and the
APPGW_CERT_SECRET_ID secret in environments/prd.yaml.
The short answer
Terraform/Bicep automate the plumbing, not the certificate itself:
Automated by Terraform/Bicep |
NOT automated by Terraform/Bicep |
|---|---|
Creating the Key Vault |
Issuing/renewing the certificate |
The user-assigned managed identity |
Uploading the certificate’s private key |
The RBAC role assignment ( |
Rotating the certificate before expiry |
The App Gateway’s |
So yes — populating the certificate is a separate action from the terraform apply/bicep deploy that creates the Key Vault and wires up access. It is either:
A one-time manual action (
az keyvault certificate import, or the portal) — fine for an internal cert, a short-lived dev cert, or a cert from a CA with no Key Vault integration.A separately-automated, ongoing process — a Key Vault-integrated CA issuer (DigiCert/ GlobalSign), or a small standalone component like Key Vault Acmebot for Let’s Encrypt — which renews the secret in Key Vault on its own schedule, entirely outside the App Gateway’s own IaC lifecycle.
Either way, the App Gateway’s Terraform/Bicep never sees the certificate bytes: it only ever
reads a keyVaultSecretId/key_vault_secret_id that already exists.
What the IaC creates
Bicep:
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: kvName
location: location
properties: {
sku: { family: 'A', name: 'standard' }
tenantId: subscription().tenantId
enableRbacAuthorization: true
}
}
resource appgwIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'id-appgw'
location: location
}
resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(kv.id, appgwIdentity.id, 'KeyVaultSecretsUser')
scope: kv
properties: {
principalId: appgwIdentity.properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User
)
}
}
resource appgw 'Microsoft.Network/applicationGateways@2023-11-01' = {
name: 'appgw-example'
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: { '${appgwIdentity.id}': {} }
}
properties: {
sslCertificates: [
{
name: 'primary-cert'
properties: {
// The secret must already exist — this apply does not create it.
keyVaultSecretId: 'https://${kvName}${environment().suffixes.keyvaultDns}/secrets/appgw-cert'
}
}
]
}
}
Terraform:
resource "azurerm_key_vault" "this" {
name = var.kv_name
location = var.location
resource_group_name = var.rg_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
enable_rbac_authorization = true
}
resource "azurerm_user_assigned_identity" "appgw" {
name = "id-appgw"
location = var.location
resource_group_name = var.rg_name
}
resource "azurerm_role_assignment" "appgw_kv_reader" {
scope = azurerm_key_vault.this.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_user_assigned_identity.appgw.principal_id
}
# Data source, not a managed resource — the secret is assumed to already
# exist. `terraform apply` never creates or rotates it.
data "azurerm_key_vault_secret" "appgw_cert" {
name = "appgw-cert"
key_vault_id = azurerm_key_vault.this.id
}
resource "azurerm_application_gateway" "this" {
# ...
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.appgw.id]
}
ssl_certificate {
name = "primary-cert"
key_vault_secret_id = data.azurerm_key_vault_secret.appgw_cert.id
}
}
Using a data source (not azurerm_key_vault_certificate) is deliberate: it keeps certificate
issuance out of the App Gateway’s own apply cycle, and keeps the private key out of Terraform
state.
Getting the certificate into Key Vault
Option A — manual, one-time import
az keyvault certificate import `
--vault-name kv-example `
--name appgw-cert `
--file ./appgw-cert.pfx `
--password <pfx-password>
Use this for an internal/private-CA cert, a dev/self-signed cert, or any CA that has no Key Vault integration. You (or whoever owns the cert) are responsible for re-running this before expiry — nothing in the IaC reminds you.
Option B — Key Vault-integrated CA issuer (semi-automated)
If you have a DigiCert/GlobalSign contract, configure it once as a Key Vault “issuer”, then let Key Vault manage the CSR, submission, and renewal itself:
az keyvault certificate issuer create --vault-name kv-example --issuer-name digicert --provider DigiCert
az keyvault certificate create --vault-name kv-example --name appgw-cert `
--policy "$(az keyvault certificate get-default-policy --output json | jq '.issuerParameters.name = "digicert"')"
Key Vault auto-renews before expiry — no manual re-import needed once configured.
Option C — Let’s Encrypt via Key Vault Acmebot (fully automated, free CA)
Deploy Key Vault Acmebot (an Azure Functions app, itself a small separate Bicep/Terraform deployment) once. It performs the ACME DNS-01/HTTP-01 challenge and writes/renews the certificate into Key Vault on a timer trigger — completely decoupled from the App Gateway’s own IaC pipeline.
Mapping this onto strata
The config/ example encodes exactly this split:
resources/networking.yaml’skey-vaultresource — the Key Vault Terraform creates.resources/networking.yaml’sapp-gateway-wafresource — declares a capabilitydependencies[]entry onkey_vault, and itsconfiguration.key_vault_secret_idis${secret:APPGW_CERT_SECRET_ID}— never a literal PFX/password.environments/prd.yaml’sAPPGW_CERT_SECRET_IDsecret (store: azure-keyvault) — names the existing secret; its owndescriptionfield is where you record which of Options A/B/C above populates it, since strata itself never issues or rotates the certificate.integrations/azure-keyvault.yaml— the integration that resolves that token at deploy time.
strata build run/deploy run only ever read the secret’s identifier — issuing or renewing the
certificate stays entirely outside strata’s own responsibility, same as it stays outside
Terraform/Bicep’s.