1行のログから
運用しているサーバーの error_log に、こんな行が出ていました。
Hostname appw.jp (default host as no SNI was provided) and hostname www.ibe.tokyo provided via HTTP have no compatible SSL setup for policy 'secure'
1文ですが、3つのことが書かれています。
なぜこんなことが起きるのか。 前回は利用者から見た証明書を扱いました。 今回はサーバー側から——証明書がどう選ばれているかを見ます。
1つのIPで、複数のサイトを動かす
いま運用している VPS では、4つのドメインが同じIPアドレスで動いています。 appw.jp、ibe.tokyo、gwaw.jp、iseeit.jp。
IPアドレスは有限で、サーバーも1台ごとに費用がかかります。 1つのIPで複数サイトを運用するのは、ごく普通のことです。
同じIPアドレス・同じポートで、ドメイン名によってサイトを切り替える仕組み。 どのサイト宛かは、リクエストの中に書かれた名前で判断します。
HTTP なら簡単だった
GET /index.html HTTP/1.1 Host: www.ibe.tokyo ← これを見れば分かる
サーバーは Host: を読んで、対応する設定を選べばよい。
それだけで済んでいました。
HTTPS で、順序の問題が生じる
前回書いたとおり、
TLS では暗号化の前に証明書を交換します。
しかし Host: ヘッダは暗号化された後に送られる。
① クライアント → サーバー 接続を開始 ② サーバー → クライアント 証明書を提示 ← どれを出す? ③ 鍵の交換・暗号化の開始 ④ クライアント → サーバー Host: www.ibe.tokyo ← ここで初めて分かる
②の時点で、サーバーはどのサイト宛か知りません。 だから SNI が必要になりました。
名前は、2回送られる
| 層 | いつ | 何を送るか | 暗号化 |
|---|---|---|---|
| TLS | 接続の最初 | SNI(server_name 拡張) | されない |
| HTTP | 暗号化の確立後 | Host: ヘッダ | される |
同じドメイン名を、2回伝えています。 ふつうは同じ値ですが、別の値を入れることもできます。 仕組み上、止められません。
[TLS] SNI: www.ibe.tokyo → ibe.tokyo の証明書を提示 [HTTP] Host: www.ibe.tokyo → 一致。処理を続行
[TLS] SNI: なし → デフォルト(appw.jp)の証明書を提示 [HTTP] Host: www.ibe.tokyo → 不一致。拒否
デモ:証明書が選ばれるまで
SNI と Host の組み合わせを変えると、どの証明書が選ばれ、結果がどうなるかが見られます。 実際の構成に合わせ、appw.jp をデフォルトの vhost としています。
「SNIなし・一致」を試してみてください。 SNI が無くても、Host がデフォルトの vhost と同じなら正常に処理されます。 SNI が無いこと自体はエラーではありません。
デフォルトの vhost とは
SNI が無いとき、サーバーは何らかの証明書を出さなければなりません。 黙って切ることもできますが、それでは古いクライアントが一切つながらなくなります。
そこでデフォルトの vhost が選ばれます。
Apache では、設定ファイルで最初に現れた vhost(または _default_)です。
$ apachectl -S
VirtualHost configuration:
*:443 is a NameVirtualHost
default server appw.jp (/etc/apache2/sites-enabled/appw.conf:1)
port 443 namevhost appw.jp
port 443 namevhost www.ibe.tokyo
port 443 namevhost www.gwaw.jp
port 443 namevhost www.iseeit.jp
default server と書かれているものが、SNI 無しのときに選ばれます。
今回の環境では appw.jp でした。
ログの (default host as no SNI was provided) は、これを指しています。
なぜ、不一致を拒否するのか
食い違っていても、Host ヘッダのほうを信じて処理を続けることは技術的に可能です。
実際、SSLStrictSNIVHostCheck を off にすれば通ります。
前回書いたとおり、 証明書が保証するのは「接続先が名乗るドメインの持ち主である」ことだけです。 その保証を成立させるには、 提示した証明書と、返す中身が一致していなければなりません。
HTTP/2 では、もう一つ理由がある
HTTP/2 では、1本の接続を複数のドメインで使い回すことがあります。 証明書が両方をカバーしていれば、接続を張り直さずに済むためです。
その際、この接続では扱えないドメイン宛のリクエストが届くことがあります。
そのときサーバーは 421 Misdirected Request を返し、
クライアントに接続を張り直すよう促します。
ログから見える相手
では、SNI を送ってこないのは誰なのか。
| 送らない相手 | 実情 |
|---|---|
| 極めて古いクライアント | Windows XP の IE など。現在ではほぼ見かけない |
| 簡易なスクリプト | SNI を設定していない自作ツール |
| スキャン・探索 | IPアドレスに直接接続し、応答を確かめる |
正規のブラウザは、20年以上前から SNI を送っています。 いま SNI 無しで来るのは、ほぼ人間の操作ではありません。
今回のログは、スキャンでした
IPアドレスに直接つないで、そこで何が動いているかを探る行為です。 Host ヘッダに適当なドメインを入れて、応答の違いを見ている。
bot の分類でも書きましたが、 名乗りは自己申告です。SNI も Host ヘッダも、クライアントが自由に書けます。 食い違いを検出する仕組みがあって初めて、意味を持ちます。
運用側で確かめること
| 確認したいこと | 方法 |
|---|---|
| デフォルトの vhost はどれか | apachectl -S |
| SNI を送ったときの証明書 | openssl s_client -servername ドメイン -connect IP:443 |
| SNI 無しのときの証明書 | openssl s_client -noservername -connect IP:443 |
| 厳格チェックの設定 | SSLStrictSNIVHostCheck(既定は on) |
# SNI あり $ openssl s_client -servername www.ibe.tokyo -connect 203.0.113.10:443 \ </dev/null 2>/dev/null | openssl x509 -noout -subject subject=CN = www.ibe.tokyo # SNI なし $ openssl s_client -noservername -connect 203.0.113.10:443 \ </dev/null 2>/dev/null | openssl x509 -noout -subject subject=CN = appw.jp ← デフォルトが返る
まとめ
この記事のまとめ
- 1つのIPで複数サイトを動かすと、どの証明書を出すかを判断する必要がある。
- ドメイン名は2回送られる。TLS の SNI(平文)と、HTTP の Host(暗号化後)。
- SNI が無いとデフォルトの vhost が選ばれる。設定ファイルで最初に現れたもの。
- SNI 無しが即エラーではない。Host がデフォルトと一致すれば通る。
- 食い違うと
AH02032。証明書と中身が違う状態を許さないための拒否。 - 緩めることはできるが、緩めれば証明書の検証が無意味になる。
- HTTP/2 の接続再利用では
421を返し、張り直しを促す。 - いま SNI を送らないのは、ほぼスキャンや自動化ツール。エラーは防御が働いた記録。