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:
- Read the full request chain — DNS → custom domain → SNI handshake → certificate → binding → HTTPS enforcement → app behind a proxy — and localise any TLS or domain failure to one link.
- Diagnose a certificate name mismatch by reading the live cert with
openssl s_clientand comparing its CN/SAN against the requested host, including the apex-vs-wwwand wildcard-coverage traps. - Fix a failed or missing binding and choose correctly between an SNI SSL binding and an IP-based SSL binding, knowing exactly what each costs and which clients force the choice.
- Pass custom-domain verification every time by getting the
asuid.TXT (or domain-verification) record and the A/CNAME records right, and by confirming propagation before you click Add. - Break an HTTPS-only redirect loop by identifying who terminates TLS and making the app honour
X-Forwarded-Protoinstead of fighting the proxy. - Use App Service Managed Certificates and Key Vault certificate references correctly, and diagnose their specific failure modes (apex/CNAME limits, renewal, identity/access).
- Set a sane minimum TLS version and avoid breaking older clients, and read the binding/cert reference tables to pick the right option for each scenario.
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:
subject=and theX509v3 Subject Alternative Name:list — the host you typed must appear literally in the SAN list. If you requestedcontoso.comand the SAN isDNS:www.contoso.com, that’s your bug.issuer=— if it readsCN = Microsoft Azure ... *.azurewebsites.net, there is no binding for your host and App Service is serving its default cert. That’s a binding problem, not a cert problem (jump to the binding section).notBefore/notAfter— rule out an expiry masquerading as a name issue.
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 shop → app-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
- Read the served cert before re-issuing anything.
openssl s_client -servername <host>shows the actual cert and SAN — most “the cert is broken” tickets are a missing binding or a SAN that doesn’t cover the host, not a bad cert. - Pre-stage DNS days before a custom-domain cutover. Create the
asuid.TXT and the routing A/CNAME ahead and confirm they resolve; never do the cutover on launch morning. - Use a multi-SAN certificate for apex +
www. A bare wildcard does not cover the apex; one multi-SAN cert avoids the most common mismatch. - Default to SNI bindings; reach for IP-based only on confirmed no-SNI clients — it costs a reserved IP and forces a DNS A-record change.
- Enforce HTTPS in exactly one layer, and always trust forwarded headers behind a proxy. If a proxy terminates TLS, enforce HTTPS at the edge and turn the app’s redirect off; whatever the topology,
UseForwardedHeaders/trust proxy/ProxyFixsoRequest.IsHttpsreflects the original scheme — this single setting prevents the redirect loop. - Store production certs in Key Vault and import by reference. Rotate in the vault; bind once. Give the app a managed identity with least-privilege certificate read.
- Prefer App Service Managed Certificates for non-prod and many prod subdomains — free and auto-renewed, just keep the DNS mapping stable or renewal fails.
- Set
minTlsVersionto 1.2 (1.3 after testing). Strong and broadly compatible; raise only once you’ve confirmed clients negotiate it. - Alert on certificate days-to-expiry, not just “site down.” Even auto-renewing certs lapse if DNS drifts; pick a canonical host (apex or
www) and redirect the other so you aren’t maintaining two equal entry points by accident.
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
- Enforce HTTPS-only and a sane minimum TLS.
httpsOnly: trueandminTlsVersion: 1.2(or 1.3 where clients allow) so traffic is never cleartext or deprecated — and never disable HTTPS-only to “fix” a redirect loop; fix the forwarded-scheme handling instead. - Keep certificates and private keys in Key Vault, imported by reference rather than uploaded into app config, so the key lives in the vault, rotation is centralised, and the app reads it via a least-privilege managed identity (
Key Vault Certificate User). - Don’t leak the apex and
wwwas two equal entry points. Pick a canonical host, redirect the other, and keep both HTTPS-valid so an attacker can’t downgrade users on the non-canonical name. - Consider HSTS once HTTPS is solid — add
Strict-Transport-Securityafter the cert/binding/redirect are correct, since it makes a broken cert much harder for users to bypass. - Use mTLS deliberately. If you enable
clientCertEnabled/clientCertMode, scopeclientCertExclusionPathsso probes and public paths aren’t blocked — and remember it manifests as a 403, not a TLS warning. - Lock down who can change bindings and DNS, and ship full-chain certs. A wrong binding or a changed
asuid/routing record breaks TLS for everyone (protect both with RBAC); an incomplete chain (ERR_CERT_AUTHORITY_INVALID) is both a reliability and a trust problem — verify withopenssl -showcerts.
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
- 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? - A
*.contoso.comwildcard cert is bound,www.contoso.comis fine, butcontoso.comshowsERR_CERT_COMMON_NAME_INVALID. Why, and what’s the fix? - True or false: you can put a CNAME on the apex (
contoso.com) to point it at<app>.azurewebsites.net. - A site loops with
ERR_TOO_MANY_REDIRECTSonly when accessed through its Front Door hostname. Name the most likely cause and the one setting in the app that fixes it. - You raised
minTlsVersionto 1.3 and some clients now get a connection reset (no cert warning). What does that tell you, and what’s the fix?
Answers
- The certificate isn’t wrong — there’s no binding. The
*.azurewebsites.netsubject 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. - A bare wildcard does not cover the apex —
*.contoso.commatcheswww/apibut notcontoso.com. Fix by adding the apex as an explicit SAN (multi-SAN cert), binding a separate apex/managed cert, or serving onwwwand redirecting the apex. - 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
asuidTXT. - A front proxy (Front Door) terminates TLS and forwards plain HTTP, while the app’s HTTP→HTTPS redirect ignores
X-Forwarded-Protoand re-redirects every request. Fix by making the app honourX-Forwarded-Proto(UseForwardedHeaders/trust proxy) so it sees the originalhttpsscheme and stops looping. - 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
minTlsVersionto 1.2 (the broadly-compatible floor) or upgrade the clients.
Glossary
- Custom domain — your own hostname (e.g.
shop.contoso.com) mapped to an App Service app; must be DNS-verified before any TLS binding works. - Custom-domain verification ID — the token (
customDomainVerificationId) you place in anasuid.<sub>TXT record to prove you control the domain’s DNS. - SNI (Server Name Indication) — the hostname the client sends in the TLS ClientHello so a multi-tenant front end can pick which certificate to present.
- SAN (Subject Alternative Name) — the list of hostnames a certificate is valid for; the requested host must appear literally or the client reports a name-mismatch. (The legacy CN/Common Name is ignored for matching by modern browsers.)
- Wildcard certificate —
*.contoso.com, valid for one label below the domain (www,api) but not the apex (contoso.com) or deeper labels. - TLS/SSL binding — the object mapping a hostname to a certificate and a type; without it App Service serves its default
*.azurewebsites.netcert. SNI SSL (the free default) selects the cert via SNI; IP-based SSL uses a dedicated inbound IP for no-SNI clients (costs an IP, changes the A record). - App Service Managed Certificate — a free, Microsoft-issued, auto-renewed TLS certificate for a verified custom domain; not exportable, with apex/wildcard caveats.
- Key Vault certificate reference — importing a certificate from Azure Key Vault into App Service by reference, using the app’s managed identity, so rotation happens in the vault.
httpsOnly— site setting that redirects plain-HTTP requests to HTTPS with a301.minTlsVersion— the lowest TLS protocol version the front end will negotiate (e.g.1.2); too high fails old clients at the handshake.X-Forwarded-Proto— the header a TLS-terminating proxy sets to tell the backend the original request scheme; the app must honour it to avoid redirect loops.- Certificate chain — the leaf plus intermediate certificate(s) the server must send so a client can build trust to a root; an incomplete chain causes
ERR_CERT_AUTHORITY_INVALID. - CAA record — a DNS record that restricts which Certificate Authorities may issue certs for the domain; a mismatched CAA blocks managed-cert issuance.
- HSTS (
Strict-Transport-Security) — a response header instructing browsers to use HTTPS for future visits to the domain.
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:
- Next: Azure DNS Public Zones: Delegation, Alias and Records Setup — master the A/CNAME/TXT/CAA and alias-record mechanics every binding and verification depends on.
- Related: Azure Key Vault: Secrets, Keys and Certificates — store and rotate production certificates the right way and import them by reference.
- Related: App Service Plans and Tiers Explained: Free to Isolated — see which TLS, custom-domain and IP-based-SSL features each tier unlocks.
- Related: Application Gateway with WAF, mTLS and End-to-End TLS — the front-proxy layer where the redirect-loop and origin-host traps live, and how to do end-to-end TLS.
- Related: Troubleshooting Azure App Service: 502/503 Errors, Cold Starts and Restart Loops — the sibling playbook for when a TLS/binding problem surfaces as a 5xx instead of a cert warning.