Server Name Indication (SNI) for multi-domain TLS hosting, routing, and configuration patterns
Scope: SNI protocol extension, multi-domain TLS hosting, routing, security implications Lines: ~290 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
Problem SNI Solves:
Before SNI (circa 2003):
Client → [TLS Handshake]
← [Certificate for ???]
Problem: Server doesn't know which certificate to send!
With SNI (2003+):
Client → [TLS Handshake + hostname: "example.com"]
← [Certificate for example.com]
Solution: Server sends correct certificate
How SNI Works:
1. Client initiates TLS handshake
2. Client includes hostname in ClientHello (SNI extension)
3. Server selects appropriate certificate based on SNI
4. Server continues handshake with correct certificate
5. Encrypted connection established
TLS Handshake with SNI:
ClientHello
├─ Version: TLS 1.2
├─ Random: [32 bytes]
├─ Cipher Suites: [...]
└─ Extensions:
└─ server_name (SNI):
├─ name_type: host_name (0)
└─ host_name: "example.com"
Viewing SNI with tcpdump:
sudo tcpdump -i any -A 'tcp port 443' | grep -i "server_name"
Multi-domain setup:
# Domain 1
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
location / {
proxy_pass http://backend1;
}
}
# Domain 2
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.example.com.crt;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
location / {
proxy_pass http://backend2;
}
}
# Domain 3
server {
listen 443 ssl;
server_name another-domain.com;
ssl_certificate /etc/ssl/certs/another-domain.com.crt;
ssl_certificate_key /etc/ssl/private/another-domain.com.key;
location / {
proxy_pass http://backend3;
}
}
SNI-based routing:
# Route based on SNI to different backends
map $ssl_server_name $backend {
example.com backend1:8080;
api.example.com backend2:8080;
admin.example.com backend3:8080;
default backend1:8080;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/ssl/certs/default.crt;
ssl_certificate_key /etc/ssl/private/default.key;
location / {
proxy_pass http://$backend;
proxy_set_header Host $ssl_server_name;
}
}
Multi-domain setup:
# Domain 1
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile /etc/ssl/private/example.com.key
</VirtualHost>
# Domain 2
<VirtualHost *:443>
ServerName api.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/api.example.com.crt
SSLCertificateKeyFile /etc/ssl/private/api.example.com.key
</VirtualHost>
# Enable SNI
SSLStrictSNIVHostCheck on
Multi-domain TLS:
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
// Load certificates for different domains
cert1, _ := tls.LoadX509KeyPair("example.com.crt", "example.com.key")
cert2, _ := tls.LoadX509KeyPair("api.example.com.crt", "api.example.com.key")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert1, cert2},
MinVersion: tls.VersionTLS12,
}
// Go automatically handles SNI with multiple certificates
server := &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Connected to: %s\n", r.Host)
}),
}
server.ListenAndServeTLS("", "")
}
Custom SNI logic:
tlsConfig := &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// Custom logic based on SNI
switch hello.ServerName {
case "example.com", "www.example.com":
return &cert1, nil
case "api.example.com":
return &cert2, nil
default:
return &defaultCert, nil
}
},
}
Set SNI explicitly:
import ssl
import socket
hostname = 'example.com'
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
# SNI automatically set from server_hostname parameter
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
print(f"SNI sent for: {hostname}")
print(f"Certificate subject: {ssock.getpeercert()['subject']}")
Requests library (SNI automatic):
import requests
# SNI automatically extracted from URL
response = requests.get('https://example.com')
# SNI automatically sent
curl https://example.com
# Test different SNI vs Host header
curl --resolve example.com:443:192.168.1.1 https://example.com
# Test SNI with different hostname
curl --connect-to example.com:443:api.example.com:443 https://example.com
Setup:
server {
listen 443 ssl;
server_name *.example.com;
# Single wildcard cert for all subdomains
ssl_certificate /etc/ssl/certs/wildcard.example.com.crt;
ssl_certificate_key /etc/ssl/private/wildcard.example.com.key;
# Route based on subdomain
location / {
set $subdomain "";
if ($host ~* "^(.+)\.example\.com$") {
set $subdomain $1;
}
proxy_pass http://$subdomain-backend:8080;
}
}
Tenant-specific certificates:
package main
import (
"crypto/tls"
"database/sql"
"net/http"
)
type CertStore struct {
db *sql.DB
}
func (cs *CertStore) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// Fetch tenant cert from database based on SNI
var certPEM, keyPEM string
err := cs.db.QueryRow(
"SELECT cert, key FROM tenant_certs WHERE domain = ?",
hello.ServerName,
).Scan(&certPEM, &keyPEM)
if err != nil {
// Return default cert
return getDefaultCert()
}
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
return &cert, err
}
func main() {
store := &CertStore{db: initDB()}
tlsConfig := &tls.Config{
GetCertificate: store.GetCertificate,
MinVersion: tls.VersionTLS12,
}
server := &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
Handler: myHandler(),
}
server.ListenAndServeTLS("", "")
}
# Catch-all for invalid SNI
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/ssl/certs/default.crt;
ssl_certificate_key /etc/ssl/private/default.key;
return 404;
}
# Specific domains
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# ... normal config
}
Problem: SNI is sent in plaintext during handshake
Eavesdropper can see:
✓ Destination hostname (from SNI)
✓ Client IP
✓ Connection timing
✗ Actual data (encrypted)
Solution: Encrypted Client Hello (ECH) / ESNI
# Enable ECH (nginx 1.25+)
ssl_ech on;
ssl_ech_key /etc/ssl/ech-key.pem;
Attack: Client sends wrong SNI to bypass routing
Mitigation:
# Verify SNI matches Host header
server {
if ($ssl_server_name != $host) {
return 421 "Mismatch Error";
}
}
Symptom: Certificate doesn't match hostname
Check SNI sent:
openssl s_client -connect example.com:443 -servername example.com -tlsextdebug
Common causes:
Affected clients:
- IE 6 on Windows XP
- Java 6
- Python < 2.7.9
- Android < 2.3
Workaround: Dedicated IP per domain
# Different IPs for each domain
server {
listen 192.168.1.10:443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
}
server {
listen 192.168.1.11:443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.example.com.crt;
}
Problem: Load balancer needs to route based on SNI
Solution (HAProxy):
frontend https_front
bind *:443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
use_backend example_com if { req_ssl_sni -i example.com }
use_backend api_example_com if { req_ssl_sni -i api.example.com }
default_backend default_backend
backend example_com
mode tcp
server server1 192.168.1.10:443
backend api_example_com
mode tcp
server server2 192.168.1.11:443
# Test with specific SNI
openssl s_client -connect 192.168.1.1:443 -servername example.com
# Test without SNI (should get default cert)
openssl s_client -connect 192.168.1.1:443 -noservername
# Test different SNI values
for domain in example.com api.example.com unknown.com; do
echo "Testing SNI: $domain"
openssl s_client -connect 192.168.1.1:443 -servername $domain 2>/dev/null | \
openssl x509 -noout -subject
done
cryptography-pki-fundamentals - Certificate basicscryptography-tls-configuration - TLS setupnetworking-load-balancing - Load balancer configurationproxies-reverse-proxy - Reverse proxy patternsLast Updated: 2025-10-27