Produced by Fourier

Apache SNIの落とし穴:旧ドメインのSSL vhostに証明書が無くて packet length too long 。暫定証明書で解決した話

Kyouhei Horizumi icon Kyouhei Horizumi

TL;DR

  • new.example.com のSSL設定は正しいのに、 openssl s_clientcurl で TLS 接続に失敗( packet length too long など)。
  • 原因は :443 の「最初に読み込まれた」VirtualHost(=デフォルトSSL vhost)が旧ドメイン(old) で、 そこに証明書が無かった ため。SNIが働く前に握手が崩壊していた。
  • 旧ドメイン側にも暫定の証明書を当てたら即解決 。以後は SNI により new/old が正しく振り分けられる。

環境

  • OS: RHEL 系(httpd / mod_ssl)
  • 目的:
    • old.example.com は「常に new.example.com へリダイレクト」
    • new.example.com は本番サイトを提供
  • 状況: old の :443 vhost が先に読み込まれていたが、 証明書が未設定
    <VirtualHost *:443>
        ServerName old.example.com
    
        # ここが肝:TLSを受けない(証明書も置かない)
    
        # 以降は意味がないが、例として置いておく(実際はTLS成立前に失敗)
        RewriteEngine On
        RewriteRule ^(.*)$ https://new.example.com$1 [R=302,L]
    </VirtualHost>
    
    <VirtualHost *:443>
        ServerName new.example.com
    
        SSLEngine on
        SSLCertificateFile    /etc/letsencrypt/live/new.example.com/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/new.example.com/privkey.pem
    </VirtualHost>

症状

  • curlopenssl s_client の SNI 付き確認でも TLS 失敗:
    curl -I --resolve new.example.com:443:SERVER_IP https://new.example.com/
    # curl: (35) TLS connect error: error:0A0000C6:SSL routines::packet length too long
  • httpd -S を見ると :80 も :443 も Default Server が old になっている

原因の正体(SNIとデフォルトSSL vhost)

  • Apache の :443 は、本来 TLS ハンドシェイク中に送られる SNI(ClientHello の server_name )で該当 vhost を切り替え ます。
  • ただし SNI を読む以前に TLS ハンドシェイクが成立しない場合 、SNI 判定まで到達できません。
  • 今回は 最初に読み込まれた :443 の vhost(=デフォルトSSL vhost)が旧ドメインで、そこに証明書が無い/ SSLEngine On で受けられない 状態だったため、 握手の入口で失敗 。結果として「SNI が合致しているはずなのに old の設定に飲まれる」ように見えました。
  • 対策として 旧ドメイン側にも暫定証明書を付与 すると、まず TLS が成立 → SNI を読めるnew.example.com へ正しく振り分けられる、という順序で解消します。

どう解決したか(実施した対応)

✅ 旧ドメイン側にも暫定の証明書を当てる

  • 正式発行前でもOK。 自己署名 でも、 new の証明書を一時流用 でも、まずは握手できればよいです。
# /etc/httpd/conf.d/110-old-ssl.conf
<VirtualHost *:443>
  ServerName old.example.com
  ServerAlias www.old.example.com
  SSLEngine on
  # 暫定の証明書(自己署名や一時SAN、もしくはとりあえず何かしら)
  SSLCertificateFile    /etc/pki/tls/certs/old-temp.crt
  SSLCertificateKeyFile /etc/pki/tls/private/old-temp.key

  RewriteEngine On
  RewriteRule ^(.*)$ https://new.example.com$1 [R=302,L]
</VirtualHost>

自己署名の作成例:

sudo openssl req -x509 -nodes -newkey rsa:2048 \
  -keyout /etc/pki/tls/private/old-temp.key \
  -out /etc/pki/tls/certs/old-temp.crt \
  -days 30 -subj "/CN=old.example.com"

これで TLS ハンドシェイクが成立し、 SNI が正しく働く ようになるので、 new.example.com での接続は new vhost に振り分けられます。

まとめ

SNI は TLS が成立して初めて 読めます。 そのため、最初に受ける :443 vhost に証明書が無いと SNI 判定に到達せず、意図と異なる(あるいは失敗する)動作に見えることになります。 そこで旧側にも暫定証明書を当てることで TLS → SNI → vhost 切替の順序が正常化し、問題は解消するというわけです。

参考文献

この問題は、Apache公式Wiki「 SSL with Virtual Hosts Using SNI 」でも触れられています。 要点として、 SSL名ベースのVirtualHostでは、最初に定義された:443のvhostが初期ハンドシェイクに使われる ため、 そこにTLS1.0以上が許可され、証明書が載っていないとSNI情報を受け取る前に失敗 します。 結果として、SNIで一致しているはずのホストに切り替わらず、“最初のvhost”側の挙動(あるいはハンドシェイク失敗)に見えるという、まさに今回の事象と一致します。 該当箇所として、 「最初(デフォルト)のSSL vhostには少なくともTLSv1.0以上を許可する必要がある」「ホスト名が届かない場合は最初のvhostが使われる」 といった説明があります。