SNI(Server Name Indication)
概要
SNI(Server Name Indication) は、TLS ハンドシェイクの ClientHello メッセージにホスト名を含めることで、1 つの IP アドレスで複数のドメインの証明書を使い分けられるようにする TLS 拡張です。RFC 6066(2011 年)で定義されています。
SNI が登場する前、TLS では 1 つの IP アドレスに 1 枚の証明書しか対応できませんでした。複数のドメインを同一サーバーでホストする場合、ドメインごとに異なる IP アドレスを割り当てる必要があり、IP アドレスの枯渇問題と設定コストの増大を招いていました。
HTTP/1.1 では Host ヘッダーによってバーチャルホストを実現しています。しかし TLS ハンドシェイクは HTTP より先に行われるため、証明書を選択する時点ではまだ Host ヘッダーを読めません。SNI はこのギャップを解消します。クライアントが接続先のホスト名を TLS ハンドシェイクの最初の段階で通知することで、サーバーは適切な証明書を選択してハンドシェイクを続けられます。
仕組み
SNI は TLS ハンドシェイクの最初のメッセージでホスト名を通知し、サーバーが適切な証明書を選択できるようにします。
TLS ハンドシェイクでの SNI の役割
TLS ハンドシェイクにおける SNI の処理順序は次のとおりです。
- クライアントが
ClientHelloを送る。このメッセージのserver_name拡張フィールドに接続先のホスト名(例:example.com)が含まれる - サーバーは
server_nameを参照して、対応する証明書と秘密鍵を選択する - サーバーが
ServerHelloと選択した証明書を返す - クライアントは証明書を検証し、ハンドシェイクを完了する
SNI がない場合、サーバーはデフォルト証明書を返すか、エラーを返します。デフォルト証明書が別のドメインのものであればホスト名検証に失敗し、ブラウザーは接続を拒否します。
サーバー側の設定
Nginx では server_name ディレクティブで SNI の照合先を設定します。複数のサーバーブロックに同じ IP と同じポートを指定すると、Nginx はクライアントから受け取った SNI 値に基づいて適切なブロックを選択します。
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
}
上記の 2 つのサーバーブロックは同一 IP の 443 ポートを共有し、SNI によってどちらの証明書を使うか決定します。
確認方法
openssl で接続時に SNI が正しく機能しているか確認するには次のコマンドを使います。
# SNI を明示的に指定して接続(-servername オプション)
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R10
-servername オプションが SNI に相当します。返ってきた証明書の subject が期待するドメインと一致していれば SNI が正しく機能しています。
SNI を指定せずに接続して何が返ってくるか確認するには次のように実行します。
# SNI なしで接続(デフォルト証明書が返る)
openssl s_client -connect 203.0.113.1:443 -noservername </dev/null 2>/dev/null | openssl x509 -noout -subject
外部の視点からも確認したい場合は、Labee Dev Toolbox の SSL Cert API を使うと、外部の視点から見た結果を取得できます。
curl "https://labee.dev/api/ssl-cert?hostname=example.com"
{
"success": true,
"data": {
"hostname": "example.com",
"port": 443,
"reachable": true,
"status": 200
},
"error": null,
"meta": { "responseTime": 123 }
}
data.reachable が true であれば SNI を含む TLS ハンドシェイクが正常に完了し、HTTPS 接続が確立されています。
よくある問題
SNI に関するトラブルは、IP 直接接続時のホスト名不一致やデフォルトサーバーの証明書選択に起因するものが中心です。
SNI を使わず IP アドレスで直接接続
curl https://203.0.113.1/ のように IP アドレスで接続すると、SNI が指定されないためデフォルト証明書が返ります。証明書のホスト名がアクセス先の IP アドレスと一致しないため、ホスト名検証に失敗します。IP アドレスでのアクセスが必要な場合は、-k フラグで検証を無効化するかホスト名を -H Host: ヘッダーで指定しますが、本番環境では避けます。
Nginx のデフォルトサーバーが意図しない証明書を返す
複数のサーバーブロックがある場合、SNI に一致するものがなければ Nginx は default_server フラグのついたブロックの証明書を返します。default_server を明示的に設定していない場合は最初に定義されたサーバーブロックがデフォルトになります。意図しない証明書が返ってきてホスト名検証が失敗するケースはこれが原因のことが多いです。
ワイルドカード証明書と SNI の組み合わせ
*.example.com のワイルドカード証明書は第 1 レベルのサブドメイン(api.example.com、www.example.com)をカバーしますが、a.b.example.com のような 2 階層以上の深さのサブドメインはカバーしません。SNI でホスト名を正しく通知できても、証明書のホスト名検証で失敗します。2 階層以上のサブドメインには SAN 証明書を使います。
HAProxy や ALB でのバックエンドへの SNI 転送
L7 ロードバランサーを経由する構成で、バックエンドにも SNI が必要な場合は設定が必要です。HAProxy では ssl verify required sni str(example.com) を指定し、AWS ALB では対象バックエンドの SNI 設定を有効化します。設定が漏れると、バックエンドサーバーがデフォルト証明書を返し、ALB がホスト名検証に失敗します。
古いクライアントとの互換性
SNI は TLS の拡張であるため、SNI をサポートしないクライアントはホスト名を含まない ClientHello を送ります。この場合、サーバーはどのドメインへのアクセスか判断できないため、デフォルト証明書(最初に定義されたサーバーブロックの証明書)を返します。
SNI のブラウザー対応は 2006 年頃から始まりました。Firefox 2.0、Internet Explorer 7 がこの世代です。現在の一般的なブラウザーやモバイル OS はすべて SNI に対応しています。
問題が起きるのは次のようなケースです。
- Windows XP の Internet Explorer 6 以前(2006 年以前のブラウザー)
- Python 2 系の
urllib2(2012 年以前のバージョン) - OpenSSL 0.9.8 以前(2005 年以前のバージョン)
- 一部の古い Java アプリケーション(JDK 1.6 未満)
現代的なシステムでは SNI の非対応クライアントを考慮する機会はほぼありません。ただし組み込みデバイス、古いネットワーク機器、レガシーシステムとの連携では注意が必要な場合があります。
ECH(Encrypted Client Hello)
SNI の課題として、ClientHello のホスト名が平文で送信されるため、通信経路上の第三者(ISP、ファイアウォール、ネットワーク機器)がアクセス先のホスト名を傍受できます。HTTPS を使っていても SNI だけで通信先のドメインが把握される点はプライバシー上の問題です。
この問題を解決するために開発されたのが ECH(Encrypted Client Hello) です。RFC 9849 で標準化されており、ClientHello 全体を暗号化します。ECH を使うと、ネットワーク上で見えるのはアウターの SNI(通常は CDN のドメイン)のみになります。Chrome 117 以降と Firefox 119 以降でデフォルト有効化されており、DNS レコード(HTTPS リソースレコード)でサーバーの ECH 公開鍵を配布するインフラが必要です。