Azure Troubleshooting

App Service TLS and Custom Domain Errors: Fixing SNI Mismatches, Failed Bindings and HTTPS-Only Redirect Loops

You bought the certificate, you mapped the domain, you flipped on HTTPS-only — and now the browser shows a red padlock, an ERR_CERT_COMMON_NAME_INVALID, or it just spins forever in a redirect loop. Every one of these is a TLS-or-custom-domain failure on Azure App Service, the platform-as-a-service that fronts your web app behind a shared multi-tenant TLS layer, and every one of them is maddening for the same reason: the error the browser prints (a cert warning, TOO_MANY_REDIRECTS, “your connection is not private”) describes a symptom three or four hops away from the cause. The certificate might be valid but bound to the wrong hostname. The domain might be “added” but never verified. The redirect loop might be App Service doing exactly what you told it while a proxy in front quietly strips the very header that would stop it.

This is the diagnostic playbook for that whole class of pain. We treat SNI mismatches, failed certificate bindings, domain-verification failures and HTTPS-only redirect loops not as four unrelated bugs but as symptoms along one chain: DNS → custom-domain hostname → TLS handshake (SNI) → certificate → binding → HTTPS enforcement → the app behind a proxy. You will learn to localise a failure to exactly one link in that chain using the tools that tell the truth — openssl s_client to see the actual certificate the server presents, az webapp config hostname and az webapp config ssl to read what App Service thinks is bound, nslookup/dig to confirm the DNS the handshake depends on, and the Custom domains and Certificates blades to see verification state. Every diagnosis comes with the exact command to confirm it and the precise fix, with both az CLI and Bicep. Because you will reach for this mid-incident, the playbook, the error codes, the binding types and the settings are all laid out as scannable tables — read the prose once, then keep the tables open when the padlock turns red.

By the end you will stop guessing. When a cert warning appears you will know within a minute whether the subject name doesn’t cover the host, the binding is missing or wrong-typed, the chain is incomplete, the domain was never verified, or an old client simply can’t do SNI. When the browser loops you will know whether it’s HTTPS-only fighting a proxy that already terminated TLS, a hand-rolled redirect rule duplicating the platform’s, or a Front Door origin misconfiguration. Knowing which in ninety seconds is what turns a launch-day fire drill into a two-minute fix.

What problem this solves

App Service makes “put my app on https://shop.contoso.com” look like three clicks: add a custom domain, add a binding, tick HTTPS-only. That abstraction is wonderful until one of the moving parts underneath — DNS records you don’t fully control, a multi-tenant TLS front end that uses SNI (Server Name Indication) to pick which certificate to present, an X.509 subject and Subject Alternative Name (SAN) that must literally contain the hostname, a certificate chain the client must be able to build to a trusted root — quietly disagrees with what you intended. When that happens the browser shows a generic security error, and a generic error against a five-link chain is exactly the kind of problem people burn an afternoon on.

What breaks without this knowledge: an engineer re-issues the certificate three times (when the real problem is a missing SAN), re-adds the custom domain repeatedly (when DNS hasn’t propagated), or “fixes” a redirect loop by turning HTTPS-only off (masking a header-forwarding bug). Meanwhile the actual cause — a TXT record under the wrong name, a cert whose SAN is www but whose host is the apex, a proxy that forwards plain HTTP so App Service redirects every request to HTTPS forever — sits there, perfectly diagnosable, ignored. It hits essentially everyone who puts a real hostname on App Service, and bites hardest on first launches (verification and DNS ordering), apex/naked-domain setups, apps fronted by Front Door or Application Gateway (the redirect-loop and host-header traps), and certificate migrations. The fix is almost never “buy a new cert” — it’s “find the link in the chain that disagrees, and make it agree.”

To frame the whole field before the deep dive, here is every symptom class this article covers, what the client is really telling you, and the one place to look first:

Symptom class What the client/browser is saying First question to ask First place to look Most common single cause
Cert name mismatch (ERR_CERT_COMMON_NAME_INVALID) “The cert presented doesn’t cover this hostname” Does the cert’s SAN list literally contain this host? openssl s_client -servername <host> Cert for www used on apex (or wildcard not covering apex)
Failed / missing binding “No cert is bound for this hostname” (default cert served) Is there a binding, and is its type right for the client? az webapp config ssl list Cert uploaded but never bound; or SNI binding hit by a no-SNI client
Domain verification fails “App Service won’t add the domain” Has DNS propagated, and is the verification record exactly right? nslookup -type=TXT asuid.<host> TXT/CNAME under wrong name or not yet propagated
HTTPS-only redirect loop (ERR_TOO_MANY_REDIRECTS) “The server keeps bouncing me 301→301→301” Who terminates TLS, and does the app see HTTP or HTTPS? Browser network tab; X-Forwarded-Proto handling Proxy terminates TLS, forwards HTTP, HTTPS-only re-redirects
Untrusted / incomplete chain “Issuer unknown / can’t build a trusted chain” Does the server send the intermediate(s)? openssl s_client -showcerts Leaf uploaded without intermediate; or .cer instead of full chain

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand App Service basics: an App Service plan is the set of VM workers (an SKU like B1, S1, P1v3) you rent, and one or more web apps run on it. You should know that every app gets a default hostname *.azurewebsites.net with a Microsoft-managed wildcard certificate, and that putting your own hostname on it requires a custom domain plus a TLS/SSL binding. Comfort with running az in Cloud Shell, reading JSON output, and basic DNS record types (A, CNAME, TXT) is assumed. A working mental model of how a TLS handshake presents a certificate helps but we will build it.

This sits in the Observability & Troubleshooting track, on the networking/edge side, and pairs tightly with a few neighbours: the tier model in App Service Plans and Tiers Explained: Free to Isolated (IP-based SSL and managed certs are tier-gated), the DNS mechanics in Azure DNS Public Zones: Delegation, Alias and Records Setup (every verification and binding step depends on records resolving), and certificate storage in Azure Key Vault: Secrets, Keys and Certificates (the production cert path). If a proxy fronts the app, Application Gateway with WAF, mTLS and End-to-End TLS covers where the redirect-loop and host-header traps live; and when a binding problem surfaces as a 5xx, Troubleshooting Azure App Service: 502/503 Errors, Cold Starts and Restart Loops is the playbook next door.

Ownership usually splits along the chain: the DNS/registrar owner holds the A/CNAME/TXT/CAA records (verification, resolution, CA pinning); PKI/security owns the certificate file (SAN, chain, validity); the app/platform team owns the custom-domain mapping, the binding, and HTTPS enforcement (httpsOnly, minTlsVersion); and the network team owns any front proxy (TLS termination, origin host header, X-Forwarded-Proto). Knowing which layer a symptom lives in tells you who to call.

Core concepts

Six mental models make every later diagnosis obvious.

The browser’s error names the symptom, not the broken link. A red padlock or ERR_CERT_* is the client’s verdict after the handshake — it does not say whether the cert is wrong, the binding is missing, the chain is incomplete, or DNS sent you to the wrong server. Reproduce that verdict at the command line (where the tools are honest) and walk the chain backward until one link disagrees.

SNI is how one IP serves many certs. App Service’s front end is multi-tenant: many customers’ hostnames resolve toward shared IPs. During the handshake the client announces the hostname it wants in the SNI (Server Name Indication) extension of the ClientHello — before any certificate is presented — and the front end uses that name to pick which cert to send. That is why an SNI SSL binding is the default, why a client that can’t send SNI gets the fallback default cert, and why openssl must be told -servername <host> or you test a different virtual host than the browser.

The certificate must literally contain the hostname. A certificate proves identity for the names in its Subject Alternative Name (SAN) extension (the legacy Common Name / CN is ignored for matching by modern browsers). If the browser requests contoso.com but the SAN is www.contoso.com only, the names don’t match and you get ERR_CERT_COMMON_NAME_INVALID — even on a freshly issued cert. A wildcard *.contoso.com covers www and api but not the apex contoso.com and not a deeper label like a.b.contoso.com. Name coverage is the single most common cause in this whole article.

A binding is a separate object from the certificate. Uploading a cert does not make it serve traffic — it just stores it. A TLS/SSL binding is the explicit mapping “for hostname H, present cert C, using type T (SNI or IP-based).” A cert can sit valid and trusted in the Certificates blade while the hostname has no binding — and then App Service serves its default *.azurewebsites.net certificate for your custom host, which mismatches the name. “Cert there but not bound” is a distinct failure from “cert wrong.”

A custom domain must be verified before it can carry traffic. To accept shop.contoso.com, App Service requires proof you control the DNS: a TXT record asuid.<subdomain> holding the app’s verification ID, plus a routing record (a CNAME to <app>.azurewebsites.net, or an A record to the inbound IP for the apex). If either is missing, wrong, or unpropagated, the Add fails — and DNS caching means “I just set it” is not “it resolves.”

HTTPS-only is a redirect, and redirects loop when the app can’t see the real scheme. With httpsOnly on, App Service answers plain-HTTP with a 301 to https:// — harmless alone. The loop appears when a front proxy terminates TLS and forwards plain HTTP to App Service, carrying the original scheme only in X-Forwarded-Proto. The platform’s built-in redirect honours that header, but a hand-rolled redirect in your app that ignores it sees “HTTP” every time and 301s again — the proxy serves that back over HTTPS, the browser follows, and you have an infinite 301→301→301 loop. The fix is never to disable security; it is to make the app trust the forwarded scheme.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:

Concept One-line definition Where it lives Why it matters to TLS/domain errors
Custom domain Your hostname mapped to the app App config + DNS Must be verified before any binding works
Domain verification ID Token proving you own the DNS App property → asuid. TXT Wrong/absent → Add domain fails
SNI Hostname sent in ClientHello TLS handshake Picks which cert the front end presents
CN / SAN Names the cert is valid for The certificate Must contain the requested host or name-mismatch
Wildcard cert *.domain covers one label The certificate Covers www, not apex or deeper labels
TLS/SSL binding Maps host → cert → type App config Missing → default cert served (mismatch)
SNI SSL binding Cert selected via SNI Binding type Default; fails for rare no-SNI clients
IP-based SSL binding Cert on a dedicated inbound IP Binding type For no-SNI clients; costs an IP, changes the A record
App Service Managed Certificate Free auto-renewed cert from Microsoft Certificates blade Free TLS; has apex/CNAME and wildcard caveats
Key Vault certificate reference Cert imported from Key Vault Certificate + identity Production cert path; identity/access can fail
httpsOnly Redirect HTTP→HTTPS Site config On + proxy mis-forwarding → redirect loop
minTlsVersion Lowest TLS the app accepts Site config Too high → old clients fail handshake
X-Forwarded-Proto Original scheme behind a proxy Request header App must honour it or it loops

The TLS and custom-domain error reference

The lookup table you scan first — every certificate/domain error you realistically see, what it means on this platform, how to confirm it, and the first fix. The ERR_CERT_* strings are Chromium’s; Firefox and Safari print different words for the same conditions.

Error / signal What it means Likely cause on App Service How to confirm First fix
NET::ERR_CERT_COMMON_NAME_INVALID Cert doesn’t cover this hostname SAN lacks the host (apex vs www, wildcard not covering apex), or no binding (default cert) openssl s_client -connect <host>:443 -servername <host>subjectAltName Bind a cert whose SAN includes the host
NET::ERR_CERT_AUTHORITY_INVALID Can’t build a chain to a trusted root Intermediate not sent; self-signed; .cer (leaf only) uploaded openssl s_client -showcerts → only 1 cert Import the full chain (PFX with intermediates)
NET::ERR_CERT_DATE_INVALID Cert expired / not yet valid Past notAfter; managed-cert renewal lapsed; clock skew `openssl … openssl x509 -noout -dates`
ERR_TOO_MANY_REDIRECTS Endless 301 loop HTTPS-only + proxy forwards HTTP; or app redirect ignores X-Forwarded-Proto Network tab: repeated 301 to same URL Honour X-Forwarded-Proto; don’t double-redirect
SSL_ERROR_NO_CYPHER_OVERLAP / handshake_failure No common protocol/cipher minTlsVersion higher than the client supports openssl s_client -tls1_2 ... vs -tls1_3 ... Lower minTlsVersion to what clients need
Default cert served on custom host No binding exists for this hostname Cert uploaded but never bound; binding deleted az webapp config ssl list shows no entry for host Create the binding (ssl bind --certificate-thumbprint)
“asuid TXT … was not found” Verification record missing/unpropagated asuid.<sub> TXT absent, wrong value, or unpropagated nslookup -type=TXT asuid.<sub>.<domain> Add exact TXT; wait for propagation; retry
“CNAME / A record was not found” Routing record missing No CNAME to <app>.azurewebsites.net (or A to inbound IP for apex) nslookup <host> / dig Add the routing record; confirm it resolves
CAA record blocks issuance A CAA record forbids the issuing CA CAA pins a different CA than the managed-cert issuer dig CAA <domain> Add a CAA entry for the issuer, or remove CAA
Wrong cert / 502 behind Front Door Origin host/SNI mismatch originHostHeader not set to the app’s hostname FD origin settings; openssl ... -servername <fd-origin> Set origin host header to the custom domain
403 “Forbidden” on HTTPS Client cert required, or a scheme/access rule clientCertEnabled (mTLS) on, or an access restriction Site config clientCertEnabled; access restrictions Present client cert, or scope clientCertExclusionPaths

Three distinctions save the most time. Name-mismatch vs no-binding both show a “wrong cert” warning, but openssl -showcerts separates them: if the served cert’s SAN is *.azurewebsites.net, there is no binding (default cert); if it’s your cert but the host isn’t in its SAN, it’s a name-mismatch. App-loop vs platform-loop: if the redirect loop persists with httpsOnly:false, the redirect is in your code (or the proxy), not the platform. Handshake-failure vs cert-warning: a handshake failure (no page, connection reset) is a protocol/cipher/minTlsVersion problem; a cert warning (you can click “proceed”) means the handshake succeeded but the cert didn’t validate.

Anatomy of a certificate name mismatch

NET::ERR_CERT_COMMON_NAME_INVALID (Chromium) / SSL_ERROR_BAD_CERT_DOMAIN (Firefox) / “the name does not match” (Safari) all mean one thing: the certificate the server presented does not list the hostname the client asked for. The distinct ways this happens on App Service are an apex requested but the cert covers only www (or vice-versa), a wildcard used on the apex (which it can’t cover), a deeper subdomain against a single-label wildcard, the wrong cert bound to the host, or no binding at all (so the default *.azurewebsites.net cert is served — confirm with openssl -showcerts; that’s a binding problem, covered in the next section). For all of them the move is the same: read the cert the server actually serves, then make its SAN cover the host.

Read the certificate the server actually presents

Stop reading the cert file you uploaded and read the cert the server is serving — they are not always the same object. openssl s_client is the ground truth, and -servername is mandatory because App Service uses SNI to pick the cert:

# What certificate does App Service present for THIS hostname? (SNI matters)
openssl s_client -connect shop.contoso.com:443 -servername shop.contoso.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

Read three lines of that output:

In the portal the same truth lives under Certificates → Bring your own certificates / Managed certificates, where each cert shows its subject and SAN list, and under Custom domains, where each hostname shows which binding (if any) serves it.

The apex-vs-www and wildcard traps, made explicit

The two mismatches that catch people every launch are apex/www and wildcard coverage, and the rules are exact. A SAN of contoso.com covers only the apex; www.contoso.com covers only www. A wildcard *.contoso.com covers every single label below the domain (www, api) but never the apex contoso.com, and never a two-label name like a.api.contoso.com (which needs *.api.contoso.com). So a wildcard alone leaves the apex warning, and a www-only cert leaves the apex warning — the most common name-mismatch there is.

The fix is to make the SAN cover the host you actually serve. If you serve both apex and www, use a multi-SAN certificate (apex + www) or a wildcard plus an explicit apex entry — a bare wildcard never covers the apex. Then bind that cert to the host. The cheapest correct path for a small site is often an App Service Managed Certificate for www and an apex redirect, or a separate managed cert per hostname (covered below).

Anatomy of a failed or missing TLS binding

A binding is the object that says “serve certificate C for hostname H.” When it’s missing, wrong, or the wrong type for the client, you get a name-mismatch (the default cert is served), a handshake the client can’t complete (a no-SNI client missing an SNI binding), or a flat refusal to create the binding (“hostname is not assigned” when the domain isn’t verified, or “certificate not found” / a SKU error below Basic). The two that need explaining are the order of operations and the type choice.

The order is fixed: domain first, then binding

The single most common binding mistake is trying to bind before the hostname is verified and assigned. You cannot bind a certificate to a hostname App Service doesn’t yet own. The order is always: (1) add and verify the custom domain, (2) ensure the certificate is present on the app, (3) create the binding. Reverse it and you get a confusing “hostname is not assigned to this site” error that looks like a cert problem but is a sequencing problem.

# 1) Confirm the hostname is added (must appear here before you can bind)
az webapp config hostname list --webapp-name app-shop-prod --resource-group rg-shop-prod -o table

# 2) Confirm the certificate is present on the app, and grab its thumbprint
az webapp config ssl list --resource-group rg-shop-prod \
  --query "[].{name:name, thumbprint:thumbprint, sniEnabled:hostNames}" -o table

# 3) Create the SNI binding (host must already be assigned, cert must already be uploaded)
az webapp config ssl bind --name app-shop-prod --resource-group rg-shop-prod \
  --certificate-thumbprint <THUMBPRINT> --ssl-type SNI

In Bicep the binding is a hostNameBindings child resource that references the certificate’s thumbprint; the dependency on the Microsoft.Web/certificates resource (sourced from Key Vault via keyVaultId/keyVaultSecretName, or uploaded via pfxBlob) must be explicit so the deploy orders correctly:

resource binding 'Microsoft.Web/sites/hostNameBindings@2023-12-01' = {
  parent: site
  name: 'shop.contoso.com'
  properties: {
    sslState: 'SniEnabled'                 // or 'IpBasedEnabled' for a no-SNI client
    thumbprint: cert.properties.thumbprint  // cert resource must exist first
    hostNameType: 'Verified'
  }
}

SNI vs IP-based SSL — choose deliberately

SNI SSL is the default and the right answer for ~99% of cases: it costs nothing extra, supports unlimited hostnames behind the shared IPs, and works with every browser and SDK of the last decade. IP-based SSL assigns the app a dedicated inbound IP and presents the cert without needing SNI — necessary only for the dwindling set of clients that don’t send SNI, but it costs a reserved IP and changes the app’s inbound IP, so you must update your DNS A record after enabling it. Choose with this table:

Dimension SNI SSL binding IP-based SSL binding
Client requirement Client must send SNI (all modern clients do) Works for clients that don’t send SNI
Inbound IP Shared App Service IPs Dedicated inbound IP assigned to the app
DNS impact None beyond the normal A/CNAME A record must point to the new dedicated IP after enabling
Cost No extra charge Charged per IP-based binding (reserved IP)
Hostnames supported Many per app One cert/IP per binding (IP is the scarce resource)
Tier required Basic (B1) and above Basic (B1) and above; IP-based has its own cost
When to use Default — virtually always Only for legacy no-SNI clients or a strict fixed-IP requirement

Enable IP-based SSL only when you’ve confirmed a no-SNI client, then repoint the A record to the new dedicated IP:

az webapp config ssl bind -n app-shop-prod -g rg-shop-prod --certificate-thumbprint <THUMBPRINT> --ssl-type IpBasedSsl
az webapp show -n app-shop-prod -g rg-shop-prod --query inboundIpAddress -o tsv   # the new A-record target

Anatomy of a domain-verification failure

App Service refuses to add shop.contoso.com until you prove DNS control. The Add operation checks for a verification TXT record named asuid.<subdomain> holding the app’s custom-domain verification ID, plus a routing record (CNAME to <app>.azurewebsites.net for a subdomain, or an A record to the inbound IP for the apex). Almost every “verification won’t pass” ticket is one of: the TXT is missing or wrong-valued; the record is set but not yet propagated (TTL/cache); the apex has a CNAME instead of an A record (“A record was not found”); the record sits under the wrong name (shop rather than asuid.shop); or a registrar UI doubled the zone suffix (asuid.shop.contoso.com.contoso.com, fixed by entering just the host part asuid.shop). Get the two records exactly right and confirm they resolve before clicking Add.

Get the verification ID and the records exactly right

The verification ID is a property of the app — read it, then build the two records around it:

# The custom-domain verification ID (goes into the asuid TXT record value)
az webapp show --name app-shop-prod --resource-group rg-shop-prod \
  --query customDomainVerificationId -o tsv

You create two records — a verification TXT and a routing record — and the routing record differs for a subdomain versus the apex (a CNAME is illegal at a zone root, so the apex needs an A record to the inbound IP, or an Azure DNS alias A that behaves CNAME-like):

Scenario Verification record Routing record
Subdomain shop.contoso.com TXT asuid.shop = <customDomainVerificationId> CNAME shopapp-shop-prod.azurewebsites.net
Apex contoso.com TXT asuid = <customDomainVerificationId> A @<app inboundIpAddress> (or Azure DNS alias A to the app resource)

Confirm propagation before you click Add — this is the step people skip:

# Verify both records resolve (query a public resolver to dodge stale caches); apex uses -type=A
nslookup -type=TXT asuid.shop.contoso.com 8.8.8.8
nslookup shop.contoso.com 8.8.8.8

Only once both resolve do you add the hostname:

az webapp config hostname add --webapp-name app-shop-prod --resource-group rg-shop-prod \
  --hostname shop.contoso.com

A note on CAA records: if your zone has a CAA record, it whitelists which Certificate Authorities may issue certs for the domain. An App Service Managed Certificate is issued by a specific CA; if your CAA pins a different CA, issuance silently fails. Check and fix with dig CAA contoso.com — either add a CAA entry permitting the managed-cert issuer or remove the restrictive CAA. The DNS mechanics behind all of this (record types, propagation, alias records, CAA) are covered in depth in Azure DNS Public Zones: Delegation, Alias and Records Setup.

Anatomy of an HTTPS-only redirect loop

ERR_TOO_MANY_REDIRECTS is the browser giving up after following too many 301s to the same place. On App Service this nearly always means HTTPS enforcement and a proxy disagree about whether the request is already secure: a proxy terminates TLS and forwards HTTP while a redirect (platform httpsOnly, or your app’s own, or a Front Door rule) re-redirects it; occasionally two redirect rules (the proxy’s and the app’s) both fire. Rarely it’s not a true 301 loop at all but mixed-content/HSTS upgrade churn from absolute http:// URLs in the page. The diagnosis below pins which.

Find out who terminates TLS and what the app sees

The whole diagnosis is one question: does the app receive HTTP or HTTPS, and does it know the original scheme? When App Service sits behind Front Door or Application Gateway, that proxy usually terminates TLS and forwards plain HTTP to the origin, setting X-Forwarded-Proto: https so the backend knows the client used HTTPS. App Service’s built-in HTTPS redirect respects that header — so platform httpsOnly alone does not loop. The loop comes from application code that redirects HTTP→HTTPS by checking Request.IsHttps (which is false because the hop from proxy to app is HTTP) without consulting X-Forwarded-Proto.

Two tests localise it. First compare the proxied hostname against the app’s direct *.azurewebsites.net URL; if the direct URL is fine but the proxied one loops, the forwarded-scheme is the cause. Then toggle platform HTTPS-only off — if the loop survives, the redirect is in your app, not the platform:

curl -sIL https://shop.contoso.com            | grep -iE "HTTP/|location"   # proxied: repeated 301s?
curl -sIL https://app-shop-prod.azurewebsites.net | grep -iE "HTTP/|location"  # direct: clean?
az webapp update -n app-shop-prod -g rg-shop-prod --https-only false
# loop gone  -> platform redirect vs proxy: keep https-only OFF, enforce HTTPS once at the edge
# loop stays -> it's YOUR app's redirect: make it honour X-Forwarded-Proto

Fix it in the right layer

The clean rule is redirect HTTP→HTTPS exactly once, at the layer that terminates TLS. If Front Door terminates TLS, enforce HTTPS there and turn off the app’s own redirect (and consider leaving platform httpsOnly off so the app never re-redirects forwarded HTTP). If nothing fronts the app, let platform httpsOnly do it and remove any app-level redirect. And whatever your topology, make the application trust the forwarded headers so framework redirects compute the scheme correctly. In ASP.NET Core that is ForwardedHeaders; every framework has an equivalent:

// ASP.NET Core: trust X-Forwarded-Proto/For so Request.IsHttps reflects the ORIGINAL scheme
app.UseForwardedHeaders(new ForwardedHeadersOptions {
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// Now app.UseHttpsRedirection() sees "https" from the proxy and does NOT re-redirect.

Where each redirect layer belongs, and what enabling it in the wrong place does:

Topology Enforce HTTPS where App-level redirect Platform httpsOnly Risk if you double up
App Service alone (no proxy) Platform Off On Two redirects, but same scheme — usually fine, still avoid
Behind Front Door Front Door rules Off Off (or honour X-Forwarded-Proto) Proxy forwards HTTP → platform/app loops
Behind Application Gateway App Gateway listener redirect Off Off (or honour X-Forwarded-Proto) Same loop as above
App does its own redirect The app (honouring X-Forwarded-Proto) On (but scheme-aware) Off App sees HTTP, redirects forever

Managed certificates, Key Vault references, and minimum TLS

Three platform features round out the picture: the free App Service Managed Certificate, the production-grade Key Vault certificate reference, and the minimum TLS version knob that can lock out old clients.

App Service Managed Certificates — free, auto-renewed, with caveats

An App Service Managed Certificate is a free, Microsoft-issued, auto-renewed TLS certificate you create for a verified custom domain in seconds (az webapp config ssl create --hostname <host>, then ssl bind the resulting thumbprint — see the lab for the exact commands) — ideal for non-production and many production subdomains. It is not a drop-in for every case; the caveats are exactly what trips people:

Caveat Detail Implication
Requires a verified custom domain Domain must be added + verified first Same DNS prerequisites as any binding
Wildcards Wildcard managed certs have their own requirements (DNS TXT validation) Don’t assume a free *.contoso.com is one click
Apex support Apex domains are supported but the domain must point to the app correctly (A/alias) Get the apex A/alias right first
Auto-renewal Renewed automatically before expiry by the platform But the domain mapping must remain valid, or renewal can fail
Not exportable You can’t download the private key Don’t pick managed certs if you must share the cert elsewhere
Tier Requires Basic (B1) or higher; not on Free/Shared Free/Shared can’t host production TLS anyway

When a managed cert “stops working” after months, the cause is almost always that the domain mapping changed (someone altered the CNAME/A record) so the platform’s renewal validation failed — fix the DNS and re-create the cert.

Key Vault certificate references — the production path

For a purchased or internally-issued certificate, the clean pattern is to store the PFX in Azure Key Vault and have App Service import it by reference, so rotation happens in the vault and the app picks up the new version. This needs the app to have an identity with get permission on the vault’s certificates/secrets. The classic failure is a missing or under-privileged identity — the import fails or the cert silently doesn’t update on rotation.

# App needs a managed identity with access to the vault's certificate (and its secret backing)
az webapp identity assign --name app-shop-prod --resource-group rg-shop-prod
PRINCIPAL=$(az webapp identity show -n app-shop-prod -g rg-shop-prod --query principalId -o tsv)

# Grant the identity rights to read the certificate (RBAC: 'Key Vault Certificate User' / 'Secrets User')
az role assignment create --assignee "$PRINCIPAL" \
  --role "Key Vault Certificate User" \
  --scope $(az keyvault show -n kv-shop-prod --query id -o tsv)

# Import the certificate from Key Vault into the app
az webapp config ssl import --name app-shop-prod --resource-group rg-shop-prod \
  --key-vault kv-shop-prod --key-vault-certificate-name shop-contoso-com

The Key Vault certificate-reference failure modes, and how each presents:

Failure mode How it presents Confirm Fix
No managed identity on the app Import fails immediately az webapp identity show returns nothing az webapp identity assign
Identity lacks vault access “access denied” / cert not imported az role assignment list --assignee <id>; vault access policies Grant Certificate/Secrets read
Vault firewall blocks the platform Import/renew fails for no obvious reason Key Vault networking blade Allow trusted Azure services / the right network
Cert rotated but app shows old cert Renewal not picked up Compare vault version vs served thumbprint Ensure reference tracks latest; re-import if pinned
PFX missing private key / password Upload/import rejected openssl pkcs12 -info on the PFX Re-export PFX with the key and correct password

The vault-side mechanics — certificate objects and their secret backing, access policies vs RBAC, soft-delete — are covered in Azure Key Vault: Secrets, Keys and Certificates.

Minimum TLS version — security vs old clients

The minimum TLS version the front end negotiates (minTlsVersion: 1.0/1.1/1.2, 1.3 increasingly available). Too high is a self-inflicted outage for legacy clients (old .NET Framework, ancient mobile OSes, some IoT) — a handshake failure, a connection reset, not a cert warning. Too low fails security scans. The sane default is 1.2; test with openssl ... -tls1_2 vs -tls1_3 (handshake success means that level is allowed) before raising the floor:

# Read and set the minimum TLS version (1.2 is the sane modern floor; in Bicep: siteConfig.minTlsVersion)
az webapp config show -n app-shop-prod -g rg-shop-prod --query minTlsVersion -o tsv
az webapp config set  -n app-shop-prod -g rg-shop-prod --min-tls-version 1.2

The minimum-TLS trade-off at a glance:

minTlsVersion Security posture Breaks which clients Use when
1.0 / 1.1 Weak; fails most scans/compliance Almost nothing Only legacy interop you can’t change — avoid
1.2 Strong; broadly compatible Very old OS/SDK (pre-2014-ish defaults) Default for virtually all apps
1.3 Strongest, faster handshake Clients without 1.3 support Modern client base, after testing

Architecture at a glance

Hold the request as a single left-to-right chain and every failure in this article maps to one hop on it. A user types https://shop.contoso.com. First the browser resolves DNS: the A/CNAME record points the name toward App Service (directly, or at a Front Door/Application Gateway that fronts it). If that record is missing or wrong, you never reach the right server — and the asuid. TXT record sitting alongside it is what App Service checked earlier to let you add the domain at all. So the first hop is DNS, and two records live there: routing and verification.

Next the browser opens a TLS connection and, in the very first packet (ClientHello), announces the hostname via SNI. App Service’s multi-tenant front end reads that name and looks up the binding for it: “for shop.contoso.com, present certificate C as an SNI binding.” If there is no binding, the front end presents its default *.azurewebsites.net certificate — which doesn’t match the name, so the browser shows a cert warning. If there is a binding, the front end presents your certificate, and the browser validates two things: that the SAN contains shop.contoso.com (else ERR_CERT_COMMON_NAME_INVALID) and that it can build a chain to a trusted root using the intermediates the server sent (else ERR_CERT_AUTHORITY_INVALID). That is the certificate hop, and it has three independent ways to fail: name, chain, validity.

Once the handshake succeeds, the request reaches the app — but if a proxy terminated TLS in front, the hop from proxy to App Service is plain HTTP, with X-Forwarded-Proto: https carrying the original scheme. Now HTTPS enforcement acts: platform httpsOnly (or your app’s redirect) must consult X-Forwarded-Proto; if it trusts the raw connection scheme instead it sees HTTP, issues a 301 to HTTPS, the proxy serves that back over HTTPS, the browser follows, and the loop is born — the final hop, where ERR_TOO_MANY_REDIRECTS lives. The whole method follows this chain: reproduce the browser’s verdict with openssl/curl, then walk DNS → SNI/binding → cert SAN/chain → forwarded-scheme until one hop disagrees. The first question on every incident — “did I even reach the right server with the right hostname?” — is answered by nslookup and openssl -servername, which already halves the search space.

Real-world scenario

Brightline Travel is launching its new booking site on Azure App Service: a Linux app (Node) on a single S1 Standard plan in Central India, to be served at both brightline.travel (apex) and www.brightline.travel. The team is three engineers; launch is scheduled for 09:00 on a Monday. They had tested everything on the *.azurewebsites.net URL for weeks. The custom-domain cutover was left for launch morning — the classic mistake.

At 08:40 they added both custom domains. www verified instantly; the apex threw “an A record was not found” because someone had created a CNAME for the apex (illegal at a zone root). Switching it to an A record to the inbound IP plus an asuid TXT verified the apex by 08:52.

They had bought a single certificate whose SAN was www.brightline.travel only — the apex was an afterthought. Bound to both hostnames, www was green but the apex showed NET::ERR_CERT_COMMON_NAME_INVALID. Reflex one: re-issue the cert. Reflex two: re-add the domain. Neither helped. At 08:58 — two minutes to launch — someone finally ran openssl s_client -connect brightline.travel:443 -servername brightline.travel | openssl x509 -noout -ext subjectAltName and read it aloud: DNS:www.brightline.travel. The cert simply didn’t contain the apex name, and a bare wildcard wouldn’t have covered it either.

The launch-morning call: serve on www and redirect the apex to www, using a free App Service Managed Certificate on the apex so the redirect endpoint was HTTPS-valid. Launch slipped to 09:08 — then every page on www spun into ERR_TOO_MANY_REDIRECTS. They had recently put Front Door in front for caching; it terminated TLS and forwarded plain HTTP, and the Node app’s own if (req.protocol !== 'https') redirect saw HTTP every time and looped forever. Disabling platform httpsOnly didn’t help — proof the loop was in the app. Trusting the proxy’s X-Forwarded-Proto (Express app.set('trust proxy', true)) made req.protocol report https, and the loop cleared at 09:21.

The permanent fix that week: a single multi-SAN certificate for apex + www, stored in Key Vault and imported by reference with a managed identity (vault-side renewals), bound to both hosts, the app-level redirect removed, and Front Door enforcing HTTPS once at the edge. The lesson on the wall: “Read the cert the server actually serves before you re-issue anything — and never cut a custom domain over on launch morning.”

The incident as a timeline, because the order of moves is the lesson:

Time Symptom Action taken Effect What it should have been
08:40 Apex won’t verify (added domains) “A record not found” Pre-stage DNS days earlier
08:52 Apex verifies Swap CNAME→A at apex Apex added Correct, just late
08:55 Apex cert warning Re-issue cert; re-add domain No change Read the served cert first
08:58 Root cause found openssl ... -ext subjectAltName → SAN is www only Mismatch confirmed This should have been step 1
09:08 Launch on www Apex managed cert + redirect to www Site live on www Reasonable launch-morning call
09:10 www loops Disable platform httpsOnly Still loops → it’s the app Honour X-Forwarded-Proto
09:21 Loop fixed trust proxy so req.protocol=https Loop clears Correct fix
+1 week Permanent Multi-SAN cert in Key Vault; FD enforces HTTPS once Clean, renewable The real fix

Advantages and disadvantages

App Service’s TLS/custom-domain model — a multi-tenant SNI front end with free managed certs, Key Vault import and one-flag HTTPS enforcement — removes almost all the work of running TLS yourself, but each convenience has an edge that produces exactly the failures above. Weigh it honestly:

Advantages (why this model helps you) Disadvantages (why it bites)
SNI lets unlimited hostnames share IPs at no extra cost — most apps never need a dedicated IP SNI fails for the rare no-SNI client, and forces the IP-based-SSL decision (cost + DNS change)
App Service Managed Certificates are free and auto-renewed — production TLS with zero PKI work Managed certs have caveats (apex/wildcard rules, not exportable) and renewal silently fails if DNS mapping drifts
Key Vault import centralises certs and rotation — bind once, rotate in the vault Needs an identity with the right access; a missing role or vault firewall blocks import with a vague error
httpsOnly enforces HTTPS with one flag, and the platform redirect honours X-Forwarded-Proto A proxy that forwards HTTP plus an app-level redirect that ignores the header loops forever
The cert/binding are first-class, readable objects (az webapp config ssl list) — state is inspectable The binding is separate from the cert, so “cert uploaded” feels done but serves the default cert until bound
Verification via DNS is standard and provable DNS propagation/caching makes “I just set it” untrue; apex needs A/alias, not CNAME — a frequent trap
minTlsVersion is a single knob to meet compliance Set too high it silently breaks old clients as a handshake failure, not an obvious error

The model is right for standard web apps and APIs that want managed TLS without operating a PKI — which is almost everyone. It bites hardest on apex/naked-domain setups, launch-day cutovers where DNS hasn’t been pre-staged, apps newly placed behind a proxy (the redirect-loop and host-header traps), and certificate migrations. Every disadvantage is manageable — but only if you know the chain and read the served cert instead of guessing, which is the whole point of this article.

Hands-on lab

Add a custom domain to an App Service app, see a deliberate name-mismatch and a missing-binding error, then fix both with a free managed certificate — all on a B1 plan you delete at the end. You need a DNS zone you control (the lab uses Azure DNS; any provider works). Run in Cloud Shell (Bash).

Step 1 — Variables and resource group.

RG=rg-tls-lab
LOC=centralindia
PLAN=plan-tls-lab
APP=app-tls-$RANDOM
ZONE=contoso.example          # a DNS zone you actually control
HOST=shop.$ZONE
az group create -n $RG -l $LOC -o table

Step 2 — Create a B1 plan and a basic app (Basic+ is required for custom domains/TLS).

az appservice plan create -n $PLAN -g $RG --is-linux --sku B1 -o table
az webapp create -n $APP -g $RG -p $PLAN --runtime "NODE:20-lts" -o table

Expected: a plan with sku.name = B1, and an app reachable at https://$APP.azurewebsites.net (served by the Azure wildcard cert).

Step 3 — Read the verification ID and the inbound IP you’ll need for DNS.

az webapp show -n $APP -g $RG --query "{verifyId:customDomainVerificationId, inboundIp:inboundIpAddress}" -o json

Step 4 — Create the two DNS records (verification + routing) in your zone. Using Azure DNS for shop:

VERIFY=$(az webapp show -n $APP -g $RG --query customDomainVerificationId -o tsv)
# Verification TXT: asuid.shop -> verification ID
az network dns record-set txt add-record -g $RG -z $ZONE -n asuid.shop -v "$VERIFY"
# Routing CNAME: shop -> <app>.azurewebsites.net
az network dns record-set cname set-record -g $RG -z $ZONE -n shop -c "$APP.azurewebsites.net"

Confirm both resolve before continuing (substitute your zone’s authoritative resolver if not Azure DNS):

nslookup -type=TXT asuid.$HOST
nslookup $HOST

Step 5 — Add the custom domain (verification must pass here).

az webapp config hostname add --webapp-name $APP -g $RG --hostname $HOST -o table

Expected: the hostname appears. Now browse to https://$HOST — you get a certificate name mismatch (ERR_CERT_COMMON_NAME_INVALID), because the domain is mapped but no binding exists, so App Service still serves its *.azurewebsites.net default cert. Prove it:

openssl s_client -connect $HOST:443 -servername $HOST </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer
# issuer/subject mention *.azurewebsites.net  ->  NO binding yet (default cert)

Step 6 — Create a free managed certificate and bind it (the fix).

az webapp config ssl create -n $APP -g $RG --hostname $HOST -o table
THUMB=$(az webapp config ssl list -g $RG --query "[?subjectName=='$HOST'].thumbprint | [0]" -o tsv)
az webapp config ssl bind -n $APP -g $RG --certificate-thumbprint "$THUMB" --ssl-type SNI -o table

Re-read the served cert — it should now be your host, valid and trusted:

openssl s_client -connect $HOST:443 -servername $HOST </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates
# subject now = CN=shop.contoso.example  ->  binding works, no more warning

Step 7 — Turn on HTTPS-only and confirm the redirect (no proxy here, so no loop).

az webapp update -n $APP -g $RG --https-only true
curl -sI http://$HOST | grep -iE "HTTP/|location"   # expect 301 -> https://$HOST

Validation checklist. You added and verified a custom domain, observed that mapping a domain without a binding serves the default cert (the name-mismatch), fixed it with a free managed cert + SNI binding confirmed by openssl, and enabled HTTPS-only. The lab steps mapped to what each proves:

Step What you did What it proves Real-world analogue
4–5 Create asuid TXT + CNAME, add domain Verification depends on exact DNS that resolves Every first custom-domain launch
5 See cert warning with domain but no binding Domain ≠ binding; default cert is served until you bind The “I added the domain, why is it warning?” ticket
6 Managed cert + ssl bind The binding is the object that fixes the mismatch The actual production fix
6 openssl before vs after Read the served cert, not the file The 90-second diagnosis
7 --https-only true + curl 301 HTTPS-only is a redirect (fine without a proxy) Where the loop would start with a proxy

Cleanup (avoid lingering plan and IP charges).

# Remove DNS records you added, then the whole resource group
az network dns record-set txt   remove-record -g $RG -z $ZONE -n asuid.shop -v "$VERIFY" 2>/dev/null || true
az network dns record-set cname remove-record -g $RG -z $ZONE -n shop -c "$APP.azurewebsites.net" 2>/dev/null || true
az group delete -n $RG --yes --no-wait

Cost note. A B1 plan is a few rupees per hour and the managed certificate is free; an hour of this lab is well under ₹50, and deleting the resource group stops everything. (B1 is the cheapest SKU that supports custom domains and TLS bindings — Free/Shared do not.)

Common mistakes & troubleshooting

This is the playbook — the part you bookmark. First a scannable table to read while the padlock is red, then the full confirm-command detail for the entries that bite hardest. It spans the simple (a missing binding, an unverified domain) and the subtle (a wildcard that doesn’t cover the apex, a proxy that loops HTTP, a Key Vault identity that can’t read the cert).

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 ERR_CERT_COMMON_NAME_INVALID on a custom host Cert SAN doesn’t contain the host (apex vs www, deeper label) `openssl s_client -connect <host>:443 -servername <host> openssl x509 -noout -ext subjectAltName`
2 Cert warning; served cert is *.azurewebsites.net No TLS binding for the host → default cert served az webapp config ssl list shows no entry for the host az webapp config ssl bind --certificate-thumbprint <t> --ssl-type SNI
3 ERR_CERT_AUTHORITY_INVALID / “issuer unknown” Intermediate not sent (leaf-only .cer uploaded) openssl s_client -showcerts ... returns only one cert Upload/import the full chain PFX (leaf + intermediates)
4 Can’t add the custom domain (“asuid TXT not found”) Verification TXT missing/wrong/unpropagated nslookup -type=TXT asuid.<sub>.<domain> 8.8.8.8 Add asuid.<sub> = customDomainVerificationId; wait for TTL; retry
5 Apex domain won’t verify (“A record not found”) Apex has a CNAME (illegal at root) instead of A/alias nslookup -type=A <apex> 8.8.8.8 returns nothing Replace with A → inbound IP (or Azure DNS alias A)
6 Apex still warns though *.domain cert is bound Bare wildcard does not cover the apex openssl ... -servername <apex> → SAN is *.domain only Add the apex as an explicit SAN, or use a separate apex cert
7 ERR_TOO_MANY_REDIRECTS only via the proxy hostname Proxy terminates TLS, forwards HTTP; redirect ignores X-Forwarded-Proto curl -sIL the proxied vs direct URL; toggle --https-only false Honour X-Forwarded-Proto; enforce HTTPS once at the proxy
8 Redirect loop persists with platform HTTPS-only OFF App’s own HTTP→HTTPS redirect checks raw scheme az webapp update --https-only false → still loops Trust forwarded headers in the app (ForwardedHeaders/trust proxy)
9 Old client gets wrong cert / can’t connect; browsers fine Client doesn’t send SNI; SNI binding can’t be matched Reproduce with a no-SNI client; openssl without -servername Add an IP-based SSL binding; repoint the A record to the new IP
10 Handshake fails (reset/no page), not a cert warning minTlsVersion higher than the client supports openssl ... -tls1_2 vs -tls1_3; az webapp config show --query minTlsVersion Lower minTlsVersion to the floor your clients need (usually 1.2)
11 Key Vault cert import fails or won’t update on rotation App identity missing or lacks vault access az webapp identity show; az role assignment list --assignee <id> Assign identity; grant Certificate/Secrets read; allow trusted services
12 Managed cert worked for months, now invalid/expired DNS mapping drifted → auto-renewal validation failed Compare current CNAME/A vs the app; managed-cert status in portal Fix the DNS record; re-create the managed cert
13 Wrong cert / 502 only when behind Front Door FD origin host header not set to the custom domain (SNI mismatch to origin) FD origin settings; openssl ... -servername <fd-origin-host> Set FD origin host header to the app’s custom hostname
14 PFX upload rejected (“no private key” / bad password) PFX exported without the key, or wrong password openssl pkcs12 -info -in cert.pfx Re-export PFX with private key + correct password
15 Managed cert creation fails for no clear reason A CAA record forbids the issuing CA dig CAA <domain> Add a CAA entry for the issuer, or remove the CAA

The table covers every row; the entries that bite hardest deserve the full reasoning. The first three rows are self-explanatory in the table — a name-mismatch is a SAN that doesn’t cover the host (re-issuing the same cert won’t help), a *.azurewebsites.net served cert is a missing binding (the cert was uploaded but never bound), and ERR_CERT_AUTHORITY_INVALID is a leaf-only upload missing its intermediates (ship a full-chain PFX). The subtler ones:

1. Adding the custom domain fails with “an asuid TXT was not found.” Root cause: The verification TXT named asuid.<subdomain> is missing, holds the wrong value, or hasn’t propagated. The apex variant fails instead with “an A record was not found” when someone put a CNAME at the zone root (illegal there) — fix that with an A record to the inbound IP or an Azure DNS alias A. Confirm: nslookup -type=TXT asuid.shop.contoso.com 8.8.8.8 and compare the value to az webapp show --query customDomainVerificationId -o tsv. Fix: Create asuid.<sub> TXT = the verification ID, wait out the record’s TTL, then retry the Add. “I just set it” is not “it resolves.”

2. ERR_TOO_MANY_REDIRECTS, but only when you browse via the proxy hostname. Root cause: A front proxy (Front Door/Application Gateway) terminates TLS and forwards plain HTTP; the HTTPS redirect then fires on every request because it isn’t reading X-Forwarded-Proto. The decisive test: toggle platform httpsOnly off — if the loop survives, the redirect is in your application (checking Request.IsHttps/req.protocol, which is HTTP on the proxy→app hop), not the platform. Confirm: curl -sIL https://shop.contoso.com | grep -iE "HTTP/|location" shows repeated 301s; the direct *.azurewebsites.net URL doesn’t loop; az webapp update --https-only false and watch whether it persists. Fix: Trust forwarded headers — UseForwardedHeaders (ASP.NET Core), app.set('trust proxy', true) (Express), ProxyFix (Flask) — and enforce HTTPS exactly once, at the edge if a proxy terminates TLS. Never disable HTTPS to “fix” it.

3. An old client gets the wrong certificate or can’t connect, while modern browsers are fine. Root cause: The client doesn’t send SNI, so the multi-tenant front end can’t match your SNI binding and serves a default/another cert. (A different failure — a flat connection reset with no cert at all — means minTlsVersion is higher than the client supports; that’s a handshake failure, not an SNI miss.) Confirm: Reproduce with a known no-SNI client, or openssl s_client -connect <host>:443 without -servername and watch the cert change. For the TLS-floor case, openssl ... -tls1_2 vs -tls1_3 shows which level negotiates. Fix: Add an IP-based SSL binding for that host (dedicated IP, no SNI needed) and repoint the A record to the new inbound IP; or, for the TLS-floor case, lower minTlsVersion to the floor your clients need (usually 1.2).

4. A Key Vault certificate won’t import, or doesn’t update after rotation. Root cause: The app has no managed identity, or the identity lacks read access to the vault’s certificate/secret, or a vault firewall blocks the platform. A version-pinned reference also won’t auto-update on rotation. Confirm: az webapp identity show -n <app> -g <rg>; az role assignment list --assignee <principalId>; check Key Vault networking. Fix: az webapp identity assign, grant Key Vault Certificate User/Secrets User, allow trusted Azure services on the vault, then re-import (untracking any pinned version).

5. Behind Front Door you get the wrong cert or a 502. Root cause: Front Door’s origin host header (originHostHeader) isn’t set to your custom domain, so the SNI/host App Service receives doesn’t match the binding. Confirm: Front Door origin settings; openssl ... -servername <the-origin-host-FD-sends>. Fix: Set the origin host header to the app’s custom hostname (and ensure that hostname is added + bound on the app).

The remaining rows in the table are self-contained: a managed cert that lapses after months means the DNS mapping drifted (fix the record, re-create the cert); a rejected PFX upload means it was exported without the private key (re-export with the key); and a managed-cert creation that fails for no clear reason is usually a CAA record pinning a different CA (dig CAA <domain>, then permit the issuer).

Best practices

The leading-indicator alerts worth wiring before the next TLS incident — not the lagging “users report a padlock”:

Alert on Signal Threshold (starting point) Why it’s leading
Certificate days-to-expiry Cert notAfter minus now < 21 days Catches a stalled renewal before users see an expired cert
Managed-cert renewal status Certificates blade / activity log Any failure event Renewal validation can fail silently on DNS drift
HTTP 301 rate spike Http3xx count Sudden sustained spike A redirect loop shows as a flood of 301s
Handshake failures TLS errors / 5xx after a minTlsVersion change Any increase post-change Old clients failing the new TLS floor
DNS record drift External monitor of the routing/asuid record Value changed unexpectedly Drift breaks both renewal and routing
4xx on the custom host Http4xx on the hostname Above baseline mTLS/access-rule or wrong-host issues surface here

Security notes

The throughline: every control here is also a reliability fix. httpsOnly plus honouring X-Forwarded-Proto stops downgrade attacks and redirect loops; a full-chain PFX prevents spoofing and ERR_CERT_AUTHORITY_INVALID outages; Key Vault import with a managed identity removes keys from config and stops manual rotation breaking the binding.

Cost & sizing

TLS and custom domains are cheap on App Service — the cost story is mostly about which features are free and the one thing that isn’t. Custom domains, SNI bindings and App Service Managed Certificates are all free: you pay for the plan you already run, not per hostname or per cert. The single TLS line item that costs money is an IP-based SSL binding, which reserves a dedicated inbound IP (billed per binding) — so enable it only for a confirmed no-SNI client. The plan tier gates the features: Free/Shared can’t host custom domains or TLS at all; Basic (B1) unlocks custom domains, SNI, IP-based SSL and managed certs; Standard/Premium add slots and scale but don’t make TLS cheaper or richer (the tier model is in App Service Plans and Tiers Explained: Free to Isolated). Key Vault adds a negligible per-operation cost and is the right home for production private keys.

For a small production site with one or two custom domains, the only TLS-specific cost on top of the plan is ₹0 with SNI + managed certificates, rising by one reserved IP only if you genuinely need an IP-based binding. The cost drivers and what each buys you:

Cost driver What you pay for Rough INR / month What it gives you Watch-out
Basic (B1) plan One Basic instance ~₹4,000–5,000 Custom domains + SNI + managed certs Free/Shared can’t do TLS at all
SNI SSL binding Nothing extra ₹0 Unlimited HTTPS hostnames on shared IPs Fails only for no-SNI clients
App Service Managed Certificate Nothing ₹0 Free, auto-renewed cert per domain Renewal fails if DNS mapping drifts
IP-based SSL binding A reserved inbound IP ~₹300–600 per binding Cert for no-SNI clients; fixed IP Changes the A record; only if truly needed
Key Vault cert storage Per-operation ~₹0–50 Centralised cert + rotation Needs identity + access set up
Purchased CA certificate Your CA’s fee (external) varies EV/OV/multi-SAN beyond managed certs Managed certs cover most needs free

Interview & exam questions

1. A custom domain serves a certificate whose name doesn’t match. How do you tell whether the cert is wrong or simply not bound? Read the served cert with openssl s_client -connect <host>:443 -servername <host> -showcerts. If the issuer/subject is *.azurewebsites.net, there is no binding and App Service is serving its default cert — fix the binding. If it’s your certificate but the host isn’t in its SAN, the certificate’s name coverage is wrong — bind a cert whose SAN includes the host.

2. Why must you pass -servername to openssl when testing App Service TLS? Because App Service’s front end is multi-tenant and uses SNI to choose which certificate to present. Without -servername, openssl sends no SNI and you test a different virtual host than the browser does — often getting the default cert and a misleading result. The browser always sends SNI, so your test must too.

3. A wildcard *.contoso.com certificate is bound, yet contoso.com shows a name-mismatch. Why? A bare wildcard does not cover the apex (*.contoso.com matches www/api but not contoso.com), and it only covers one label deep. Add the apex as an explicit SAN, use a multi-SAN cert, or serve on www and redirect the apex.

4. What’s the difference between an SNI SSL binding and an IP-based SSL binding, and when do you need the latter? SNI SSL (the default) selects the cert via the SNI hostname, supports many hostnames on shared IPs, and costs nothing extra. IP-based SSL assigns a dedicated inbound IP and presents the cert without SNI — needed only for clients that don’t send SNI (a few legacy stacks) or a hard fixed-IP requirement; it costs a reserved IP and changes the app’s inbound IP, so you must update the DNS A record.

5. App Service won’t let you add shop.contoso.com. What two DNS records does verification need, and why can’t the apex use a CNAME? A TXT record named asuid.shop holding the app’s customDomainVerificationId, and a routing record — a CNAME shop → <app>.azurewebsites.net for a subdomain. The apex can’t use a CNAME (illegal at a zone root that already has SOA/NS records), so use an A record to the inboundIpAddress or an alias/ANAME (Azure DNS alias A) plus the asuid TXT. Confirm with nslookup -type=TXT asuid.shop.contoso.com against a public resolver before retrying the Add.

6. A site behind Front Door loops with ERR_TOO_MANY_REDIRECTS. What’s happening, and how do you tell whether it’s the platform or your app? Front Door terminates TLS and forwards plain HTTP, setting X-Forwarded-Proto: https. A redirect that checks the raw scheme sees HTTP and 301s every request, looping. Diagnose by toggling platform httpsOnly off: if the loop stops it was the platform; if it persists it’s your app’s redirect. Fix by making the app honour X-Forwarded-Proto (UseForwardedHeaders/trust proxy) and enforcing HTTPS once at the edge — never by turning HTTPS-only off.

7. What does an App Service Managed Certificate give you, and what are its main caveats? A free, auto-renewed Microsoft-issued cert for a verified custom domain. Caveats: it requires the domain be added/verified, has specific rules for wildcards and apex, is not exportable, needs Basic+, and its auto-renewal fails if the DNS mapping drifts — so keep the routing record stable.

8. You see ERR_CERT_AUTHORITY_INVALID. What’s the likely cause on App Service and how do you confirm? The server is sending only the leaf cert (no intermediate), so the client can’t build a trusted chain — usually a leaf-only .cer was uploaded instead of a full-chain PFX. Confirm with openssl s_client -showcerts returning a single cert; fix by importing a full-chain PFX.

9. After raising minTlsVersion to 1.3, some clients can no longer connect (no cert warning, just a reset). What happened? Those clients don’t support TLS 1.3, so the handshake fails before any certificate is exchanged — which is why it’s a connection reset, not a cert warning. Lower minTlsVersion to the floor your clients support (usually 1.2), or upgrade the clients.

10. A Key Vault-imported certificate doesn’t update after you rotate it in the vault. What do you check? Verify the app has a managed identity (az webapp identity show) with read access to the vault’s certificate/secret (az role assignment list), that the vault firewall allows the platform, and that the reference tracks the latest version (a version-pinned reference won’t auto-update). Fix the identity/access or re-import.

These map to AZ-204 (Developer Associate)configure App Service for custom domains and TLS/SSL bindings, secure app configuration with Key Vault — and AZ-104 (Administrator)configure App Service custom domains and certificates, manage SSL bindings. The DNS and front-proxy angles touch AZ-700 (custom domains, Front Door/App Gateway TLS), and the certificate/identity security angle touches AZ-500. A compact cert-mapping for revision:

Question theme Primary cert Exam objective area
SNI vs IP-based bindings, name-mismatch AZ-204 / AZ-104 Configure TLS/SSL bindings on App Service
Custom-domain verification, apex DNS AZ-104 / AZ-700 Configure custom domains; DNS
Managed certs, Key Vault import AZ-204 / AZ-500 Secure app config; manage certificates
HTTPS-only, X-Forwarded-Proto, redirect loop AZ-204 Configure App Service; troubleshoot
minTlsVersion, chain, HSTS AZ-500 Implement platform protection; TLS
Front Door / App Gateway origin host AZ-700 Design & implement TLS at the edge

Quick check

  1. A custom host shows a cert warning. You run openssl s_client -servername <host> and the served certificate’s subject is *.azurewebsites.net. Is the certificate wrong, and what do you do?
  2. A *.contoso.com wildcard cert is bound, www.contoso.com is fine, but contoso.com shows ERR_CERT_COMMON_NAME_INVALID. Why, and what’s the fix?
  3. True or false: you can put a CNAME on the apex (contoso.com) to point it at <app>.azurewebsites.net.
  4. A site loops with ERR_TOO_MANY_REDIRECTS only when accessed through its Front Door hostname. Name the most likely cause and the one setting in the app that fixes it.
  5. You raised minTlsVersion to 1.3 and some clients now get a connection reset (no cert warning). What does that tell you, and what’s the fix?

Answers

  1. The certificate isn’t wrong — there’s no binding. The *.azurewebsites.net subject means App Service is serving its default cert because no TLS binding exists for your hostname. Create the binding (az webapp config ssl bind --certificate-thumbprint <t> --ssl-type SNI) for the host.
  2. A bare wildcard does not cover the apex*.contoso.com matches www/api but not contoso.com. Fix by adding the apex as an explicit SAN (multi-SAN cert), binding a separate apex/managed cert, or serving on www and redirecting the apex.
  3. False. DNS forbids a CNAME at the zone apex. Use an A record to the app’s inbound IP, or an alias/ANAME (Azure DNS alias A) that behaves like a CNAME at the apex, plus the asuid TXT.
  4. A front proxy (Front Door) terminates TLS and forwards plain HTTP, while the app’s HTTP→HTTPS redirect ignores X-Forwarded-Proto and re-redirects every request. Fix by making the app honour X-Forwarded-Proto (UseForwardedHeaders / trust proxy) so it sees the original https scheme and stops looping.
  5. A connection reset with no cert warning means the handshake failed before any certificate was exchanged — those clients don’t support TLS 1.3. Lower minTlsVersion to 1.2 (the broadly-compatible floor) or upgrade the clients.

Glossary

Next steps

You can now localise any App Service TLS or custom-domain failure to a hop in the chain and fix it. Build outward:

AzureApp ServiceTLSCustom DomainSNICertificatesHTTPSTroubleshooting
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading