Skip to main content

Secure a custom domain with a managed certificate

The default *.azurewebsites.net hostname on Azure App Service is already protected by a platform certificate. A production hostname that you own needs its own DNS mapping and certificate. In this lab, you map a real public subdomain, issue a free App Service managed certificate, bind it with Server Name Indication (SNI), and require modern HTTPS.

You must own or control a public DNS domain to complete this lab. DNS ownership cannot be simulated, and a made-up hostname cannot receive a publicly trusted certificate.

Estimated time: 35 to 60 minutes, plus DNS propagation and certificate issuance time.

Objectives

By the end of this lab, you will be able to:

  • Choose between a free managed certificate and bring-your-own-certificate options.
  • Map a public custom hostname to an App Service app without weakening domain ownership validation.
  • Create and bind an App Service managed certificate.
  • Enable HTTPS-only and require TLS 1.2 or later for the app and SCM endpoint.
  • Verify DNS, HTTP redirect behavior, the certificate, and the App Service configuration.

Prerequisites

Before you begin, you will need an Azure subscription with Owner permissions and a GitHub account.

In addition, you will need the following tools installed on your local machine:

Setup Azure CLI

Start by logging into Azure by run the following command and follow the prompts:

az login --use-device-code
tip

You can log into a different tenant by passing in the --tenant flag to specify your tenant domain or tenant ID.

A real domain and paid App Service plan are required

Do not continue with a production domain unless you are authorized to change its DNS. This lab uses a temporary subdomain such as asl-tls.example.com, not the root domain or an active production hostname.

Custom TLS bindings require Basic B1 or higher. This lab uses a B1 plan, about USD 13/month if left running. The managed certificate is free, but your domain registration and DNS provider can have separate costs. Delete the test resources and DNS records when you finish.

How DNS validation and TLS fit together

The DNS record sends clients to App Service. The asuid TXT record proves that your subscription controls the target app and helps prevent a dangling DNS record from being claimed by another app. After App Service accepts the hostname, it asks a public certificate authority to validate the domain and issue the managed certificate.

The mapping and certificate behavior are the same for Windows and Linux App Service plans. Runtime language does not affect the certificate.

Choose the certificate type first

RequirementBest fit
One public root domain or subdomain, basic server TLS, automatic renewalApp Service managed certificate used in this lab
Wildcard hostname such as *.example.comApp Service certificate or another public certificate
Export the certificate or use the private key outside App ServiceBring your own certificate (BYOC)
Control certificate authority, key lifecycle, or certificate policyBYOC, commonly stored in Azure Key Vault
Use the certificate as a client certificateBYOC
Private DNS in multitenant App ServiceA public managed certificate is not suitable

Managed certificates are not exportable, do not support wildcards, and can change issuer and key material during renewal. Do not pin clients to their certificate or issuer. Automatic renewal depends on keeping the supported DNS mapping and hostname binding in place.

Optional concept: Key Vault and BYOC

For BYOC, store a password-protected PFX certificate in Azure Key Vault and import it into App Service. Grant only the App Service resource provider access required for the import, and let App Service synchronize later certificate versions. BYOC changes certificate procurement, rotation, and incident-response responsibilities, so it is outside this executable lab. Never commit the PFX password or certificate private key.

Provision resources

Choose your path
Set the tooling once. Every matching step below follows your choice.
Tooling

Create or reuse a web app on a B1 or higher App Service plan. Choose one path. The azd template creates and owns its resource group. The Azure CLI and portal paths use the shared lab resource group.

Use the repository sample to provision a Linux B1 plan and deploy a web app:

git clone https://github.com/Azure-Samples/app-service-labs.git
cd app-service-labs/samples/zava-widgets

SUFFIX=$(openssl rand -hex 3)
azd auth login
azd env new "asl-tls-${SUFFIX}" --location eastus
azd up

Read the generated app and resource group names:

export RG_NAME=$(azd env get-value RESOURCE_GROUP_NAME)
export APP_NAME=$(azd env get-value WEB_APP_NAME)

azd provisions the app, but it does not own records at an arbitrary external DNS provider. The domain-validation and certificate operations below therefore use the App Service management commands after azd up. Keep the azd environment so azd down can remove the Azure resources later.

Confirm the default HTTPS endpoint responds before changing DNS:

DEFAULT_HOSTNAME=$(az webapp show \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--query defaultHostName -o tsv)

curl --fail --silent --show-error \
"https://${DEFAULT_HOSTNAME}/" \
--output /dev/null \
--write-out "HTTP %{http_code}\n"

Expected output:

HTTP 200

Step 1: Choose a temporary hostname

Prefer a subdomain because a CNAME follows the stable App Service hostname and does not depend on an inbound IP address. Set the real hostname that you control:

export CUSTOM_HOSTNAME=asl-tls.example.com

Replace example.com. Do not type the https:// scheme.

Read the target hostname and ownership verification ID:

DEFAULT_HOSTNAME=$(az webapp show \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--query defaultHostName -o tsv)

VERIFICATION_ID=$(az webapp show \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--query customDomainVerificationId -o tsv)

printf "CNAME target: %s\nTXT value: %s\n" \
"$DEFAULT_HOSTNAME" "$VERIFICATION_ID"

Step 2: Create the public DNS records

Create these records at the authoritative DNS provider for your domain:

TypeName for asl-tls.example.comValue
CNAMEasl-tlsthe value in $DEFAULT_HOSTNAME
TXTasuid.asl-tlsthe value in $VERIFICATION_ID

The CNAME must point directly to the app's *.azurewebsites.net hostname for this lab. An intermediate CNAME, proxy, or traffic service can prevent managed certificate issuance.

Wait for public DNS to return both records. Use one of these commands:

dig +short "$CUSTOM_HOSTNAME" CNAME
dig +short "asuid.${CUSTOM_HOSTNAME}" TXT

The CNAME result must end in azurewebsites.net, and the TXT result must match the app's verification ID. DNS propagation can take minutes or hours according to your provider's time-to-live (TTL).

Mapping a root domain

For a root such as example.com, use an A record that points to the app's inbound IP address and a TXT record named asuid. Root records are more sensitive to IP-address changes. The executable path in this lab intentionally uses a subdomain and CNAME.

Step 3: Add the custom hostname

Because the hostname depends on external DNS, add it after azd up:

az webapp config hostname add \
--webapp-name "$APP_NAME" \
--resource-group "$RG_NAME" \
--hostname "$CUSTOM_HOSTNAME"

Step 4: Create and bind the managed certificate

The App Service managed-certificate CLI command is currently in preview. Certificate issuance is asynchronous, so request the certificate, wait for Azure to return its thumbprint, and then bind it to the hostname:

az webapp config ssl create \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--hostname "$CUSTOM_HOSTNAME" \
--output none

CERT_THUMBPRINT=""
for attempt in {1..30}; do
CERT_THUMBPRINT=$(az webapp config ssl list \
--resource-group "$RG_NAME" \
--query "[?contains(hostNames, '$CUSTOM_HOSTNAME')].thumbprint | [0]" \
--output tsv)
if [[ -n "$CERT_THUMBPRINT" ]]; then
break
fi
echo "Waiting for certificate issuance ($attempt/30)..."
sleep 10
done

if [[ -z "$CERT_THUMBPRINT" ]]; then
echo "Certificate issuance did not finish within 5 minutes." >&2
exit 1
fi

az webapp config ssl bind \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--hostname "$CUSTOM_HOSTNAME" \
--certificate-thumbprint "$CERT_THUMBPRINT" \
--ssl-type SNI

Step 5: Enforce HTTPS and modern TLS

Apply the secure settings to the app. The sample already declares HTTPS-only and app TLS 1.2 in Bicep; this command also sets SCM TLS 1.2 explicitly:

az webapp update \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--set httpsOnly=true

az webapp config set \
--name "$APP_NAME" \
--resource-group "$RG_NAME" \
--min-tls-version 1.2 \
--generic-configurations '{"scmMinTlsVersion":"1.2"}'

Verify

Verify the mapping from outside Azure, not only in the portal.

  1. Confirm HTTP redirects to HTTPS:

    curl --head "http://${CUSTOM_HOSTNAME}/"

    Expect a 301, 302, 307, or 308 response with an HTTPS Location.

  2. Confirm HTTPS succeeds and the app responds:

    curl --fail --silent --show-error \
    "https://${CUSTOM_HOSTNAME}/" \
    --output /dev/null \
    --write-out "HTTP %{http_code}\n"

    Expected output:

    HTTP 200
  3. Inspect the presented certificate:

    echo | openssl s_client \
    -connect "${CUSTOM_HOSTNAME}:443" \
    -servername "$CUSTOM_HOSTNAME" 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates

    Confirm the subject covers your hostname, the issuer is publicly trusted, and the validity dates include today.

  4. Confirm the App Service binding and security settings:

    az webapp config hostname list \
    --webapp-name "$APP_NAME" \
    --resource-group "$RG_NAME" \
    --query "[?name=='$CUSTOM_HOSTNAME'].{hostname:name,sslState:sslState,thumbprint:thumbprint}" \
    -o table

    az webapp show \
    --name "$APP_NAME" \
    --resource-group "$RG_NAME" \
    --query "{httpsOnly:httpsOnly}" \
    -o table

    az webapp config show \
    --name "$APP_NAME" \
    --resource-group "$RG_NAME" \
    --query "{minTls:minTlsVersion,scmMinTls:scmMinTlsVersion}" \
    -o table

    Expect SniEnabled, a nonempty thumbprint, httpsOnly set to true, and both TLS values set to 1.2 or higher.

Remove DNS before deleting the app

Delete the CNAME and asuid TXT records from your DNS provider first, then wait until a public DNS lookup no longer returns them. Deleting the app while its CNAME still targets azurewebsites.net creates a dangling DNS window.

Cleanup

Run this from samples/zava-widgets to delete the resources owned by the azd environment:

azd down --purge --force

Wait for deletion, then verify the resource group is gone:

while [ "$(az group exists --name "$RG_NAME")" = "true" ]; do
sleep 10
done
az group exists --name "$RG_NAME"

Expected output:

false

Summary

You mapped a real public subdomain to App Service, proved domain ownership with an asuid TXT record, created and bound a free managed certificate, and enforced HTTPS with TLS 1.2 or later. You also learned when a managed certificate is not enough and a Key Vault-backed BYOC design is more suitable.

Troubleshooting

  • Hostname validation fails. Query the public authoritative DNS records. The CNAME must resolve directly to the app's default hostname, and the asuid TXT value must match customDomainVerificationId. Remove surrounding quotation marks added as literal TXT content.
  • Certificate stays pending or creation fails. Confirm the hostname is already mapped, the public CNAME still points directly to App Service, and no Certification Authority Authorization (CAA) record blocks DigiCert. Some domains need a CAA record that permits digicert.com.
  • A proxy or content delivery network hides the App Service hostname. Complete certificate issuance with the supported direct DNS mapping first, or terminate TLS at the proxy with a certificate managed there.
  • HTTPS returns the wrong certificate. Make the request with the custom hostname, not the app's IP address. SNI uses the hostname to choose the certificate.
  • The page returns 404 after DNS changed. Flush the local DNS cache or try a resolver that has the new record. Verify that the hostname appears in the web app's custom domains list.
  • A root domain does not accept CNAME. Use the app's inbound IP for an A record and use asuid for the ownership TXT record, or use a DNS provider that supports standards-compliant alias records at the zone apex.

Learn more