SNI (Server Name Indication): How It Works and Why It Still Matters

SNI is one of those TLS features that quietly made modern HTTPS hosting practical. Most people never think about it until something breaks, and then it becomes the entire problem.

I usually notice SNI only during troubleshooting: the wrong certificate shows up, a test succeeds against the IP but fails against the hostname, an old client cannot connect, or a load balancer presents a default certificate that nobody expected. Once you understand where SNI fits into the handshake, those failures make a lot more sense.

This guide covers the problem SNI solved, how it works in the handshake, what its privacy limitations are, and how I test and troubleshoot it on real servers.

The problem before SNI

Before SNI, hosting multiple HTTPS sites on one IP address was awkward.

With plain HTTP, a client connects and sends a Host header telling the server which virtual host it wants. HTTPS is different because the TLS handshake happens before the encrypted HTTP request. That means the server has to choose a certificate before it sees the HTTP Host header.

If one IP address served multiple domains, the server had no clean way to know which certificate to present during the handshake. The standard answer used to be “one certificate per IP address” or “one certificate covering many names.” Both approaches had costs.

SNI fixed that by letting the client declare the intended hostname in the TLS handshake itself.

If you want the wider handshake context, this TLS handshake breakdown is the right foundation.

How SNI works in the TLS handshake

SNI is sent by the client inside the ClientHello message.

The flow looks like this:

  1. client connects to 203.0.113.10:443
  2. client sends ClientHello
  3. inside ClientHello, the SNI extension includes the desired hostname, such as www.example.com
  4. server selects the matching virtual host and certificate
  5. handshake continues with that certificate

The crucial point is that traditional SNI is sent in plaintext. The rest of the session may become encrypted, but the requested server name is visible to anyone who can inspect the initial handshake.

That detail matters for privacy and is one reason newer work such as ESNI and later ECH exists.

Why plaintext SNI has privacy implications

Because classic SNI is not encrypted, it exposes the hostname the client wants to reach even when the rest of the session uses HTTPS.

That means:

  • network observers can often see the destination hostname
  • filtering systems can match on hostnames
  • the privacy benefits of HTTPS are limited at the metadata layer

People sometimes overstate or understate this. SNI is not the only metadata signal on a network, but it is a very useful one.

ESNI and ECH

ESNI was an earlier attempt to encrypt the SNI field. It has largely been superseded by ECH (Encrypted ClientHello), which aims to protect more of the ClientHello metadata rather than only the server name.

Current practice is to think in terms of ECH, not ESNI. If you are reading older articles that focus on ESNI as the future, that guidance is dated.

Operationally, though, most troubleshooting today still deals with plain SNI because that is what most environments expose and what most server administrators actively configure around.

How the server chooses the right certificate

On an SNI-capable server, the hostname from ClientHello is matched to a virtual host definition.

If there is a match, the server presents the certificate configured for that name. If there is no match, the server usually falls back to a default virtual host and presents that certificate instead.

That fallback behavior explains a lot of “wrong certificate” incidents.

Typical causes:

  • missing virtual host entry for the hostname
  • DNS points to the wrong frontend
  • load balancer listener uses the wrong certificate map
  • test omitted the SNI value, so the default cert appeared
  • wildcard or SAN assumptions did not match the real hostnames

For a broader certificate naming discussion, this SAN guide is useful, especially when deciding whether one certificate should cover several names.

Nginx and SNI

Nginx uses server_name to match hostnames and determine which server block should handle the request.

A simple HTTPS server block looks like this:

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/ssl/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/app.example.com/privkey.pem;

    root /var/www/app;
}

With multiple TLS sites on one IP, you typically have multiple server blocks with different server_name values and certificates.

A few practical notes:

  • the first loaded TLS vhost may become the de facto default if no match occurs
  • catch-all defaults should be intentional, not accidental
  • wildcard server_name handling can surprise you if you assume it covers more than it does

Validate the config:

sudo nginx -t

Inspect the live server names loaded by Nginx:

sudo nginx -T | grep -E 'server_name|listen 443'

I use that second command often when I inherit a messy config tree and want to see what names are actually defined.

For complete TLS server setup context, this Nginx SSL guide is a good companion.

Apache and SNI

Apache relies on its virtual host configuration and ServerName / ServerAlias directives.

Example:

<VirtualHost *:443>
    ServerName app.example.com
    ServerAlias www.app.example.com

    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/app.example.com.crt
    SSLCertificateKeyFile /etc/ssl/private/app.example.com.key
    SSLCertificateChainFile /etc/ssl/certs/chain.pem

    DocumentRoot /var/www/app
</VirtualHost>

Modern Apache supports SNI well, but the same logic applies: if the requested hostname does not match the vhost you think it should, Apache can present the wrong certificate or default vhost.

Check the parsed vhost map:

sudo apachectl -S

That command is worth memorizing. It tells you which name maps to which vhost, which is often faster than staring at include files for twenty minutes.

Testing SNI with openssl s_client

This is the command I reach for first:

openssl s_client -connect example.com:443 -servername example.com

The -servername flag sets the SNI hostname explicitly. Without it, you may get the default certificate and draw the wrong conclusion.

To display the full presented chain:

openssl s_client -connect example.com:443 -servername example.com -showcerts

To inspect just the leaf certificate details:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

If you want a fuller troubleshooting workflow, openssl s_client debugging goes deeper.

Testing SNI with curl

curl is useful because it combines TLS and HTTP behavior.

Basic test:

curl -Iv https://example.com/

Test a hostname against a specific IP while still sending the correct SNI and Host header:

curl -Iv --resolve example.com:443:203.0.113.10 https://example.com/

That command is excellent for pre-cutover validation. I use it when DNS has not changed yet but I want to prove the new frontend has the right certificate and vhost mapping.

If you connect by IP directly:

curl -Iv https://203.0.113.10/

expect certificate name mismatches or the default certificate unless the setup is specifically designed for that case.

Common SNI failures and what they usually mean

SNI issues rarely announce themselves as “SNI is wrong.” The error messages are often indirect.

Wrong certificate presented

This usually means one of:

  • SNI hostname not sent by the client
  • hostname not mapped on the server
  • default vhost taking over
  • load balancer certificate binding mismatch

Start by comparing these two commands:

openssl s_client -connect example.com:443
openssl s_client -connect example.com:443 -servername example.com

If the certificate changes, the issue is immediately obvious: SNI matters and your first test omitted it.

wrong version number

A common OpenSSL error looks like:

SSL routines:ssl3_get_record:wrong version number

Despite the wording, this often does not mean the peer literally chose the wrong TLS version. I have seen it caused by:

  • speaking TLS to a plain HTTP port
  • reverse proxy misrouting
  • load balancer forwarding cleartext where TLS was expected
  • STARTTLS confusion on mail protocols
  • hitting port 80 with an HTTPS client

It can appear during SNI troubleshooting because people test the wrong port, the wrong backend, or a TLS-terminating proxy that is not configured where they think it is.

Validate the port first:

curl -I http://example.com:80/
curl -Iv https://example.com:443/

Hostname mismatch errors

These usually mean the certificate does not contain the requested name in SANs or wildcard coverage does not match the label structure you assumed.

Example inspection:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

Intermittent wrong certificate

This often points to multiple edge nodes not sharing the same configuration, or a load balancer pool where one member was not updated.

I have seen this during partial rollouts: two nodes had the new cert, one still had the old default vhost.

Old client support and historical limits

SNI is not universal in every legacy client.

The old example everyone remembers is Internet Explorer on Windows XP, especially IE8 on XP, which did not support SNI properly for modern HTTPS hosting scenarios. That mattered a lot when shared-IP HTTPS hosting became common.

In current operations, this only matters if you support very old embedded systems, legacy Java runtimes, ancient appliances, or museum-grade desktops. Most public-facing services can ignore that client class now.

Still, when a stakeholder says “it only fails on one old device model,” lack of SNI support belongs on the shortlist.

SNI in load balancers, reverse proxies, and CDNs

SNI decisions are often made before traffic reaches your origin.

Load balancers

At the load balancer, SNI may determine:

  • which certificate is presented to the client
  • which backend pool receives the traffic
  • which ALPN policies or security settings apply

In these environments, fixing the origin server alone may do nothing if the front-end listener is wrong.

Reverse proxies

When a reverse proxy connects upstream over HTTPS, it may also need to send SNI to the backend. If it does not, the upstream server can present a default certificate and break validation.

For example, with curl testing a proxy chain, I explicitly check both sides if possible.

CDNs

CDNs heavily rely on SNI because shared edge IPs are normal. A certificate mapping mistake at the CDN edge can produce a perfect-looking origin setup and still serve the wrong cert publicly.

That is why I separate origin validation from public endpoint validation. They are not the same test.

Default certificates, catch-all hosts, and why they cause confusion

In real deployments, the server nearly always has some kind of fallback certificate. That is the certificate you see when the hostname does not match any SNI mapping, when a client does not send SNI, or when traffic lands on the wrong listener entirely. I have seen administrators forget that this fallback exists, then spend time inspecting the “bad” certificate even though the real problem was routing.

On Nginx, Apache, cloud load balancers, and CDNs, I prefer making the default behavior explicit. If there is a catch-all TLS host, I want to know what certificate it uses and whether it returns a clean error page, closes the connection, or exposes some unrelated application. An accidental default vhost can leak names you did not mean to expose or simply make debugging much harder.

When I inherit a system with multiple certificates on one IP, I test at least three cases:

  • the expected hostname with correct SNI
  • the bare IP with no hostname expectations
  • a nonsense hostname mapped to the same IP when the test environment allows it

That tells me very quickly what the fallback path looks like and whether the platform is selecting certificates deliberately or by accident.

Troubleshooting workflow I actually use

When an HTTPS virtual host looks wrong, I do not start by editing configs. I verify the path.

1. Check DNS

dig +short example.com

Make sure you are testing the expected IP.

2. Compare without and with SNI

openssl s_client -connect example.com:443
openssl s_client -connect example.com:443 -servername example.com

This quickly tells you whether the test itself was incomplete.

3. Inspect the certificate SANs

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

4. Test by IP with explicit hostname mapping

curl -Iv --resolve example.com:443:203.0.113.10 https://example.com/

This isolates DNS propagation from TLS and vhost logic.

5. Review vhost maps locally

Nginx:

sudo nginx -T | grep -E 'server_name|listen 443'

Apache:

sudo apachectl -S

6. Check the edge platform

If a CDN, WAF, or load balancer sits in front, inspect its certificate binding and hostname routing there too.

Security considerations

SNI is operationally necessary, but remember its limits.

  • Classic SNI leaks the hostname in plaintext.
  • The certificate selection decision happens very early, so mistakes show up before HTTP routing logic can help you.
  • Misplaced reliance on default virtual hosts can expose internal or unrelated certificates.

I prefer explicit mappings, clean SAN coverage, and regular external validation.

A quick note on deprecated and current client behavior

Older guidance around SNI often focused on ancient desktop browsers. Today the more common edge cases are not IE8 on XP but old Java runtimes, embedded libraries, industrial devices, and odd vendor appliances that either omit SNI or implement it badly. The symptom is the same: the endpoint works in a browser and fails in one stubborn client.

That is why I still ask for the exact client stack during incidents. “It does not support modern TLS” and “it does not send SNI” lead to different fixes, and people often blur them together.

Deprecated assumptions vs current practice

Deprecated assumption: one IP per HTTPS site is the normal way

That was common before SNI. It is no longer the standard model for most hosting.

Deprecated focus: ESNI as the near-term answer

ECH is the current direction. Older ESNI-specific discussions are mostly historical now.

Current practice: always test with explicit SNI when troubleshooting

If you skip -servername in openssl s_client, you are often not testing the thing users are actually hitting.

Conclusion

SNI solved a real problem: choosing the right certificate for multiple HTTPS sites sharing one IP. It works by putting the intended hostname in the ClientHello, which lets the server or edge platform select the correct virtual host before HTTP begins. That design made modern shared HTTPS hosting possible, but it also means the hostname is visible in plaintext and that certificate selection failures appear early and sometimes confusingly.

In practice, most SNI troubleshooting comes down to a few habits: test with openssl s_client -servername, use curl --resolve when checking specific frontends, inspect SAN coverage, and verify mappings on the actual TLS-terminating layer. I still use those steps constantly because SNI problems rarely fix themselves by guesswork.

Scroll to Top