Edit

Configure keyless TLS termination with Azure Key Vault Managed HSM

Azure Application Gateway can terminate TLS by using a certificate whose private key remains in Azure Key Vault Managed HSM. This approach keeps private key material inside the HSM while still allowing Application Gateway to handle TLS offload and re-encryption to backend servers.

For the conceptual background, see What is keyless TLS for Azure Application Gateway and Azure Key Vault Managed HSM?

In this tutorial, you learn how to:

  • Provision and initialize an Azure Key Vault Managed HSM instance
  • Create managed identities and assign HSM roles
  • Generate a certificate backed by an HSM private key
  • Configure Application Gateway to use the HSM-backed certificate
  • Monitor keyless TLS operations with diagnostics

If you already have an Application Gateway, a Managed HSM, and a Linux VM that can run OpenSSL 3.0.13, you can skip the provisioning steps and begin at the certificate generation section.

Note

This article uses Azure CLI for most tasks and Azure portal steps only where the portal experience is the most direct option. For a portal-first quickstart, see Create an application gateway with TLS termination.

Prerequisites

Before you begin, ensure you have:

You also need these roles during setup:

  • Managed HSM Crypto Officer for initial Azure Key Vault Managed HSM setup
  • Managed HSM Crypto User for certificate operations
  • Owner or User Access Administrator for role assignments

Important

The examples in this article use placeholder values such as <your-resource-group-name>. Replace them with values that match your environment.

Provision Azure Key Vault Managed HSM

Create the resource group and provision the Azure Key Vault Managed HSM instance.

az group create --name $RESOURCE_GROUP --location $LOCATION

export oid=$(az ad signed-in-user show --query id -o tsv)

az keyvault create --hsm-name $MHSM_NAME --resource-group $RESOURCE_GROUP --location $LOCATION --administrators $oid --retention-days 7

Generate three security domain certificates and initialize the security domain.

openssl req -newkey rsa:2048 -nodes -keyout cert_0.key -x509 -days 365 -out cert_0.cer -subj "/C=US/ST=WA/L=Redmond/O=MyCompany/CN=myapp.example.com"
openssl req -newkey rsa:2048 -nodes -keyout cert_1.key -x509 -days 365 -out cert_1.cer -subj "/C=US/ST=WA/L=Redmond/O=MyCompany/CN=myapp.example.com"
openssl req -newkey rsa:2048 -nodes -keyout cert_2.key -x509 -days 365 -out cert_2.cer -subj "/C=US/ST=WA/L=Redmond/O=MyCompany/CN=myapp.example.com"

az keyvault security-domain download --hsm-name $MHSM_NAME --sd-wrapping-keys ./cert_0.cer ./cert_1.cer ./cert_2.cer --sd-quorum 2 --security-domain-file $SECURITY_DOMAIN

If you use Azure Cloud Shell, package the generated files for download.

tar -czvf mhsm.tar.gz ./*

Grant HSM access and create the key

Assign yourself the Managed HSM Crypto User role so you can create and manage keys.

az keyvault role assignment create --hsm-name $MHSM_NAME --role "Managed HSM Crypto User" --assignee-object-id $oid --assignee-principal-type MSI --scope /keys

Verify that the identity you use for Azure CLI can access the HSM keys.

Tip

If an identity should only access a single key, scope the role assignment to /keys/<key-name> instead of /keys.

az keyvault key list --hsm-name $MHSM_NAME

Create the HSM key that is used to sign TLS operations.

az keyvault key create --hsm-name $MHSM_NAME --name $PRIVATE_KEY --ops wrapKey unwrapKey sign --kty RSA-HSM --size 2048

Prepare the Linux VM for certificate operations

Provision an Ubuntu VM and enable a system-assigned managed identity. For the portal steps, see Create a Linux virtual machine in the Azure portal and Enable a system-assigned managed identity on an existing VM.

After you enable the identity, record the object ID and grant the VM access to the HSM.

export smioid="<system-managed-identity-object-id>"

az keyvault role assignment create --hsm-name $MHSM_NAME --role "Managed HSM Crypto User" --assignee-object-id $smioid --assignee-principal-type MSI --scope /keys

Copy the TLS offload library to the VM, and then install the build dependencies required for OpenSSL.

chmod 755 ~/appgw_akv.so

sudo apt update && sudo apt upgrade -y
sudo apt install build-essential zlib1g-dev -y

Build OpenSSL and configure the TLS offload library

Build OpenSSL 3.0.13, install the TLS offload library, and configure the environment variables required by the engine.

cd /usr/local/src
sudo wget https://www.openssl.org/source/openssl-3.0.13.tar.gz --no-check-certificate
sudo tar -xvzf openssl-3.0.13.tar.gz
cd openssl-3.0.13

sudo ./config --prefix=/opt/openssl-3.0.13 --openssldir=/opt/openssl-3.0.13 shared zlib
sudo make -j$(nproc)
sudo make install

Copy the engine binary into the OpenSSL engine directory and verify the hash if you have a published checksum.

sudo cp ~/appgw_akv.so /opt/openssl-3.0.13/lib64/engines-3/

export LD_LIBRARY_PATH=/opt/openssl-3.0.13/lib64:$LD_LIBRARY_PATH
export OPENSSL_ENGINES=/opt/openssl-3.0.13/lib64/engines-3
export PATH=/opt/openssl-3.0.13/bin:$PATH

openssl engine -t -c -vvvv e_akv
openssl version

Warning

OpenSSL 3.0.13 is required for this library version. Earlier or later versions might not load the engine correctly.

Generate a self-signed certificate backed by the HSM private key and base64-encode the certificate for later use in Application Gateway.

export OPENSSL_CERT_DAYS=365
export OPENSSL_CERT_SUBJ=/CN=contoso.com

cd ~

openssl req -new -x509 -engine e_akv -keyform engine -key managedHsm:$MHSM_NAME:$PRIVATE_KEY -out cert_rsa.pem -subj $OPENSSL_CERT_SUBJ -days $OPENSSL_CERT_DAYS
base64 -w0 cert_rsa.pem > base64.txt

Important

Keep the certificate file and the base64 output in a secure location. The certificate is used to update the Application Gateway configuration, but the private key remains in Managed HSM.

Configure Application Gateway

Use an existing Application Gateway or create a new test deployment. The following example shows the optional test environment, including a Container Apps backend.

Create a test backend and Application Gateway

Create the network, Container Apps environment, and a sample backend application.

az network vnet create --name $VNET_NAME --resource-group $RESOURCE_GROUP --location $LOCATION --address-prefix $ADDRESS_PREFIX --subnet-name $APPGW_SUBNET --subnet-prefix $APPGW_SUBNET_PREFIX
az network vnet subnet create --name $BACKEND_SUBNET --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --address-prefix $BACKEND_SUBNET_PREFIX

az containerapp env create --name $ACA_ENV --resource-group $RESOURCE_GROUP --location $LOCATION --logs-destination none
az containerapp create --name $ACA_NAME --resource-group $RESOURCE_GROUP --environment $ACA_ENV --image nginxdemos/hello --target-port 80 --ingress external

ACA_URL=$(az containerapp show --name $ACA_NAME --resource-group $RESOURCE_GROUP --query properties.configuration.ingress.fqdn --output tsv)

Create the public IP, self-signed certificate, and Application Gateway.

az network public-ip create --resource-group $RESOURCE_GROUP --name $APPGW_PIP_NAME --sku Standard

openssl req -x509 -newkey rsa:2048 -keyout mykey.key -out mycert.crt -days 365 -nodes -subj "/C=US/ST=WA/L=Redmond/O=MyCompany/CN=myapp.example.com"
openssl pkcs12 -export -out $SSCERT_FILE -inkey mykey.key -in mycert.crt -passout pass:$SSCERT_PASSWD

az network application-gateway create --name $APPGW_NAME --location $LOCATION --resource-group $RESOURCE_GROUP --sku Standard_v2 --capacity 2 --public-ip-address $APPGW_PIP_NAME --vnet-name $VNET_NAME --subnet $APPGW_SUBNET --priority 100

Configure the certificate, listener, backend pool, and routing rule.

az network application-gateway ssl-cert create --resource-group $RESOURCE_GROUP --gateway-name $APPGW_NAME --name $SSCERT_NAME --cert-file $SSCERT_FILE --cert-password $SSCERT_PASSWD
az network application-gateway frontend-port create --resource-group $RESOURCE_GROUP --gateway-name $APPGW_NAME --name $HTTPS_PORT --port 443
az network application-gateway http-listener create --resource-group $RESOURCE_GROUP --gateway-name $APPGW_NAME --name $LISTENER --frontend-ip "appGatewayFrontendIP" --frontend-port $HTTPS_PORT --ssl-cert $SSCERT_NAME

az network application-gateway address-pool create --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --name $BACKEND_POOL --servers $ACA_URL
az network application-gateway http-settings create --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --name $HTTPS_SETTING_NAME --port 443 --protocol Https --cookie-based-affinity Disabled --host-name-from-backend-pool true
az network application-gateway probe create --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --name $PROBE_NAME --protocol Https --path "/" --interval 30 --timeout 30 --host-name-from-http-settings true
az network application-gateway http-settings update --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --name $HTTPS_SETTING_NAME --probe $PROBE_NAME --port 443 --protocol Https --host-name-from-backend-pool true

RULE_NAME=$(az network application-gateway rule list --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --query "[0].name" --output tsv)
az network application-gateway rule update --gateway-name $APPGW_NAME --resource-group $RESOURCE_GROUP --name $RULE_NAME --http-listener $LISTENER --http-settings $HTTPS_SETTING_NAME --address-pool $BACKEND_POOL --priority 100

Test the deployment before you move on to keyless TLS.

APPGW_PUBLIC_IP=$(az network public-ip show --resource-group $RESOURCE_GROUP --name $APPGW_PIP_NAME --query ipAddress --output tsv)

echo "Container App URL: $ACA_URL"
echo "Application Gateway public IP: $APPGW_PUBLIC_IP"

Tip

If the Application Gateway is unreachable from your network, check for NSG rules that block inbound HTTPS traffic to the gateway subnet.

Configure a multi-site listener

If you want to route requests by hostname, create a second listener and routing rule.

az network application-gateway http-listener create --resource-group "$RESOURCE_GROUP" --gateway-name "$APPGW_NAME" --name "$MULTISITE_LISTENER_NAME" --frontend-ip "appGatewayFrontendIP" --frontend-port "$HTTPS_PORT" --ssl-cert "$SSCERT_NAME" --host-name "$MULTISITE_HOSTNAME"

az network application-gateway rule create --resource-group "$RESOURCE_GROUP" --gateway-name "$APPGW_NAME" --name "rule2" --rule-type Basic --http-listener "$MULTISITE_LISTENER_NAME" --address-pool "$BACKEND_POOL" --http-settings "$HTTPS_SETTING_NAME" --priority 50

curl -vk "https://<application-gateway-public-ip>/" -H "Host: <hostname>"

Note

The multi-site listener uses the Host header to select the listener. In production, point DNS records for each hostname to the Application Gateway public IP address.

Caution

At this stage, the Application Gateway still uses the local certificate from the .pfx file. The next section replaces that certificate with a Managed HSM-backed certificate.

Configure keyless TLS

Create a user-assigned managed identity for Application Gateway and grant it access to the Managed HSM key.

az identity create --name $MI_NAME --resource-group $RESOURCE_GROUP --location $LOCATION

export mioid=$(az identity show --name $MI_NAME --resource-group $RESOURCE_GROUP --query principalId --output tsv)

az keyvault role assignment create --hsm-name $MHSM_NAME --role "Managed HSM Crypto User" --assignee-object-id $mioid --assignee-principal-type MSI --scope /keys

Tip

If the Application Gateway identity should only access one key, scope the role assignment to /keys/<key-name>.

Associate the user-assigned managed identity with Application Gateway so it can authenticate to Managed HSM.

export identityId=$(az identity show --name $MI_NAME --resource-group $RESOURCE_GROUP --query id -o tsv)

az network application-gateway update --name $APPGW_NAME --resource-group $RESOURCE_GROUP --set identity='{"type":"UserAssigned","userAssignedIdentities":{"'"$identityId"'":{}}}'
az network application-gateway show --name $APPGW_NAME --resource-group $RESOURCE_GROUP --query identity

Add a new reference to the managed HSM in sslCertificates.

# Add reference to the HSM-backed key and the base64-encoded public certificate
az network application-gateway ssl-cert create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name mHSM-Key001 \
--hsm keyId="https://<HSM NAME>.managedhsm.azure.net/keys/<key-name>" publicCertData="Q......................=="

Create a new listener with reference to the managed HSM configuration.

# Associate certificate to a new listener
az network application-gateway http-listener create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name mhsm-listener \
--frontend-ip appGwPublicFrontendIpIPv4 \
--frontend-port port_443 \
--ssl-cert mHSM-Key001 \
--host-name contoso.com

After deployment completes, verify that the Application Gateway configuration shows the new SSL certificate name in the sslCertificates array.

Note

The keyless TLS configuration keeps the private key in Azure Key Vault Managed HSM while Application Gateway performs the TLS signing operations during the handshake.

Monitor keyless TLS operations

Optional: enable diagnostics on the Managed HSM to review cryptographic operations generated by Application Gateway.

  1. Open the Managed HSM resource in the Azure portal.
  2. Select Diagnostic settings under Monitoring.
  3. Add a diagnostic setting that sends the AuditEvent category to a Log Analytics workspace.
  4. Save the setting and wait a few minutes for logs to appear.

Generate traffic and then query the logs.

for i in {1..10}; do
    curl -k --resolve contoso.com:443:<appgw-frontend-ip-address> "https://contoso.com"
done
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where Category == "AuditEvent"
| where OperationName == "KeySign"
| project TimeGenerated, OperationName, CallerIPAddress, identity_claim_appid_g, properties_s
| order by TimeGenerated desc

Tip

Use the query results to confirm that Application Gateway is calling Managed HSM for signing operations during TLS handshakes.

Troubleshooting

Disabled listener behavior

A disabled listener can occur in the following scenarios:

  • Application Gateway can't authenticate to Managed HSM because of incorrect RBAC permissions.
  • Application Gateway can't decrypt TLS handshake material because the referenced key was deleted.

Limitations

  • Listeners must use TLS 1.2 or later.
  • Supported SSL policies are limited to AppGwSslPolicy20220101, AppGwSslPolicy20220101S, and CustomV2.
  • For throughput and capacity planning, review Azure Key Vault Managed HSM scaling guidance and Azure Key Vault Managed HSM service limits.
  • Certificate replacements on the Managed HSM object might not be applied on the first update. If this condition occurs, make another small change to Application Gateway, such as updating a tag, or remove and re-add the Managed HSM reference in separate operations.
  • The following scenarios aren't supported:
    • Application Gateway Ingress Controller (AGIC)
    • WebSockets
    • L4 TCP/TLS proxy
    • FIPS mode
    • EC keys

Next steps

To expand this setup, review listener-specific SSL policy guidance and Application Gateway diagnostics.