Apache VirtualHostとSNIの設定
VirtualHostとSNIの概要
現在、Webのアクセスには「HTTP 1.1」というプロトコルが一般的に利用されています。 元となる「HTTP 1.0」が1996年5月に発表され、その僅か8ヵ月後に「HTTP 1.1」にバージョンアップされたプロトコルです。 いろいろな違いがある1.0と1.1ですが「バーチャルホスト」が使えるようになったのは1.1からです。
HTTP 1.0では、URLが「http://wiz-code.private/index.html」の場合、Webサーバー(wiz-code.private)に対して次のような要求が送信されます。
GET /index.html HTTP/1.0
上記のように、WebサーバーにはURLのパス部分のみしか要求されず、「wiz-code.private」というホスト名は含まれません。 ホスト名からIPアドレスを検索する手続きは、その手前のDNSという仕組みで完結しているためです。 Webサーバーにとっては、閲覧者が「IPアドレスを得るためにどのホスト名を検索したか」という過程は無関係なわけです。 どのホスト名に対する要求であるかが判別できないので、複数Webサイトをホスト名で分けることができません。
対してHTTP 1.1では、次のようにHOSTヘッダが必須となります。
GET /index.html HTTP/1.1
Host: wiz-code.private
ホスト名を示す「Host」ヘッダが必須項目となったことで、異なる2つ以上のホスト(ドメイン)が同じIPアドレスを示していても判別することができます。 この機構を「バーチャルホスト」と呼びます。
当然、Apacheでもバーチャルホストを利用することが出来ます。 この章では、現在までに作成した「wiz-code.private」というドメインに加えて「blog.wiz-code.private」というサブドメインを使ったサイトを設定します。 さらに、それぞれがSSLを使用できるようにSNIによる電子証明書設定を行います。
SNI(Server Name Indication)
実は、HTTPSプロトコルによるSSL接続にも、HTTP 1.0と1.1のような違いがあります。 SSLは、通信を暗号化するためのプロトコルで、閲覧者から見て、Webサーバーの手前に位置する「関所」のようなものです。
閲覧者がHTTPSで始まるURLを要求すると、ブラウザはURLのホスト(IPアドレス)に対してセキュリティ通信を始めるための「ハンドシェイク」という処理を行います。 この「ハンドシェイク」で行われる処理には、ホスト名が含まれていません。 「wiz-code.private」と「blog.wiz-code.private」という2つのホストが、同一のIPアドレスに設定され、それぞれが電子証明書を持っているとします。 このとき2つのURL「https://wiz-code.private」と「https://blog.wiz-code.private」では、ハンドシェイク処理が全く同じになります。 ホスト名が含まれませんから、サーバーはどちらのURLでハンドシェイクが要求されているのかが判別できません。 2種類の電子証明書があっても、どちらを使って通信すればよいのか、が決定できないのです。
この問題を解決するため、HTTPを1.1にバージョンアップしたときと同じように、ハンドシェイクでホスト名を含めるように改良されたものが「SNI」です。 SNIの登場によって、それまでIPアドレス1つに対して1つしか割り当てられなかった電子証明書が、ホスト名の分だけ割り当てられるようになり、SSLサイトの導入がぐっとリーズナブルになりました。 ただし、これは接続しようとするホスト名だけは暗号化されずに開示されてしまうので、「通信のすべてを暗号化して保護する」という指針からすると若干外れてしまいます。 とはいえ、接続先のIPアドレスが分かれば、DNSの逆引きによってある程度ホスト名は調べられるので、さほど大きな問題ではありません。
SNIの規格自体は2003年に出来上がっていましたが、この仕組みはWebサーバーとクライアント(Webブラウザ)の両方が対応しなければならないため、普及に時間がかかりました。 現在、クライアントは問題なく利用できるレベルに達していますが、Windows XP以前のInternet Explorerや一部の携帯電話は未対応のままです。 Webサーバー側は、Apache 2.2.12以降、IIS 8以降でSNIに対応しています。
VirtualHostの設定
Apacheで「wiz-code.private」と「blog.wiz-code.private」の2種類のホスト名を使い、別々のサイトにアクセスできるように設定します。 データは、それぞれのホスト名のディレクトリに配置します。 wiz-code.privateは「/var/www/wiz-code.private/」ディレクトリに、blog.wiz-code.privaeは「/var/www/blog.wiz-code.private/」ディレクトリです。

始めに、データを保管するディレクトリを作ります。
# mkdir /var/www/wiz-code.private
# mkdir /var/www/blog.wiz-code.private

次に、それぞれのディレクトリに「index.html」を作成し、ホスト名のみを表示します。 これは、VirtualHostがちゃんと動作しているか確認するためのものです。
# vi /var/www/wiz-code.private/index.html <!DOCTYPE html> <html> <h1>wiz-code.private</h1> </html>

# vi /var/www/blog.wiz-code.private/index.html <!DOCTYPE html> <html> <h1>blog.wiz-code.private</h1> </html>

Apacheのバーチャルホストに関する設定ファイルを編集します。 ファイルは「/etc/httpd/conf.d/virtualhost.conf」とします。 ファイルがなければ作成。
# vi /etc/httpd/conf.d/virtualhost.conf <VirtualHost *:80> ServerName wiz-code.private DocumentRoot /var/www/wiz-code.private </VirtualHost> <VirtualHost *:80> ServerName blog.wiz-code.private DocumentRoot /var/www/blog.wiz-code.private </VirtualHost>
ひとつのバーチャルホストに対して「<VirtualHost>」を作成し、ServerNameでホスト名を、DocumentRootでディレクトリを指定します。 「*:80」は、「すべてのIPアドレスに対する80番ポート」を意味します。 サーバーマシンに複数のIPアドレスが割り当てられている場合は、この「*」にIPアドレスを指定することで、IPアドレス単位のバーチャルホストを定義できます。

最後に、Apacheを再起動させます。
なお、ドメイン「blog.wiz-code.private」がCentOSを参照するようにDNSサーバーへ設定していますが、解説は省略します。

ブラウザでそれぞれのURLにアクセスすると、ちゃんとそれぞれのホスト名が表示されています。 ここまでは、Windows版のApacheでも設置したことがあるのでスムーズ。
SNIの設定
次に、SSLで接続しています。 まずは「https://wiz-code.private」に接続してみましょう。

む…。 バーチャルホストが効いてない。
あ、そっか。 virtualhost.confで80番ポートしか指定してないから…。
virtualhost.confのそれぞれの要素に、443ポートも追加します。

# vi /etc/httpd/conf.d/virtualhost.conf <VirtualHost *:80 *:443> ServerName wiz-code.private DocumentRoot /var/www/wiz-code.private </VirtualHost> <VirtualHost *:80 *:443> ServerName blog.wiz-code.private DocumentRoot /var/www/blog.wiz-code.private </VirtualHost>

Apacheも再起動して、今度は成功です。

では「https://blog.wiz-code.private/」はどうでしょう。 予想では「ホスト名が一致しない」という警告が発せられるはずです。
"The certificate is only valid for wiz-code.private(証明書は「wiz-code.private」にのみ有効です)"と言われました。 これは、SSLの目的である「接続先のサーバーが要求したURLのホスト名と一致することの証明」に起因します。
Webブラウザは、SSLによる接続を確立する際、サーバーから送られる電子証明書の「Common Name」が、URLのホスト名と一致しているかどうか、を検証します。 Webサーバーには、パスとホスト名が要求として渡されますが、そのサーバーが本当に目的のサーバーである保証はありません。 もしかしたら、通信経路のどこかでネットワーク機器がハッキングされ、意図しないサーバーにデータが送られている可能性もあるからです。
SSLによる通信では、目的のサーバーから返送される電子証明書で、URLとサーバーのホスト名が一致していることを検証します。 そのため、今回のように「blog.wiz-code.private」にアクセスしたはずなのに、証明書のCommon Nameが「wiz-code.private」だった場合、ホスト名が違うという理由で警告が発せられます。 これは、例えドメインのサブドメインであっても、です。 なお、サブドメインが多数存在する場合はワイルドカードを使って「*.wiz-code.private」とすることで、すべてのサブドメインを有効にする証明書を作ることもできます。 レンタルサーバーで、共用SSLを提供しているサービスでよく見かけます。
SNIの設定
ここからが本命。 電子証明書を「wiz-code.private」に加えて、「blog.wiz-code.private」も利用できるようにする方法です。 まずは「blog.wiz-code.private」の電子証明書を作るため、秘密鍵と署名要求を作成します。 端末は/sslディレクトリに移動しています。

# cd /home/wiz/ssl # openssl genrsa 2048 > blog.wiz-code.private.key # openssl req -new -key blog.wiz-code.private.key > blog.wiz-code.private.csr

次に、署名要求に署名します。 今回も自己署名で。
# openssl x509 -req -days 365 -signkey blog.wiz-code.private.key -in blog.wiz-code.private.csr -out blog.wiz-code.private.crt

以上で電子証明書の作成完了です。 秘密鍵は/etc/pki/tls/private/ディレクトリに、証明書は/etc/pki/tls/certs/ディレクトリにそれぞれコピーします。
# cp blog.wiz-code.private.key /etc/pki/tls/private/ # cp blog.wiz-code.private.crt /etc/pki/tls/certs/
前回は、Apacheで使う証明書をssl.confで指定しました。 あの設定は、ホスト名による指定がありません。 ならば、証明書の指定をVirtualHostディレクティブ内に書けばいい、ということでしょう。 virtualhost.confを次のように書き換えてみます。

# vi /etc/httpd/conf.d/virtualhost.conf <VirtualHost *:80 *:443> ServerName wiz-code.private DocumentRoot /var/www/wiz-code.private SSLCertificateFile /etc/pki/tls/certs/wiz-code.private.crt SSLCertificateKeyFile /etc/pki/tls/private/wiz-code.private.key </VirtualHost> <VirtualHost *:80 *:443> ServerName blog.wiz-code.private DocumentRoot /var/www/blog.wiz-code.private SSLCertificateFile /etc/pki/tls/certs/blog.wiz-code.private.crt SSLCertificateKeyFile /etc/pki/tls/private/blog.wiz-code.private.key </VirtualHost>

あれ? 証明書が切り替わらない…。 ん~…。 これは、既定の設定が優先されているのかな。 ssl.confに書かれている証明書の設定をコメントアウトしてみよう。

# vi /etc/httpd/conf.d/ssl.conf 100 #SSLCertificateFile /etc/pki/tls/certs/wiz-code.private.crt 107 #SSLCertificateKeyFile /etc/pki/tls/private/wiz-code.private.key

こんどはApacheの再起動でエラー…。
Job for httpd.service failed because the control process exited with error code. See "systemctl startus httpd.service" and "jurnalctl -xe" for details.
systemctlコマンドでステータスを確認しろ、と言われたけど、これといって明確なエラー内容は見当たらない。 まぁ、十中八九原因はssl.confでしょうね。 ssl.confの設定内容を一度精査しましょう。
ssl.conf
- Listen 443 https
- 443ポートを待ち受けにする。
- SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
- 秘密鍵がパスワードで保護されている場合に、パスフレーズを入力するダイアログの種類。 秘密鍵はパスワード保護していないので、これは関係ないですね。
- SSLSessionCache shmcb:/run/httpd/sslcache(512000)
- SSLセッションのキャッシュ設定。 これも特にいじらない。
- SSLSessionCacheTimeout 300
- SSLセッションキャッシュのタイムアウト。 このままでよし。
- SSLRandomSeed startup file:/dev/urandom 256
- 乱数のシードですね。 これもこのままでいいかな。
- SSLRandomSeed connect builtin
- こっちもシード。 startupは起動時、connectは接続時、ということですね。
- SSLCryptoDevice builtin
- 暗号化デバイス? ハードウェアのSSLアクセラレータを使う場合に設定するらしい。 そんなもの持ってないのでこのまま。
っと、ここまでは問題なさそう。 ここから先は<VirtualHost>ディレクティブになっている。 あれ、じゃぁこっちに書かないとダメ? ひとまずこの中身も順番に見ていこう。
- ErrorLog logs/ssl_error_log
- これはエラーログの保存ディレクトリ。
- TransferLog logs/ssl_access_log
- こっちはアクセスログの保存ディレクトリ。
- LogLevel warn
- ログレベル。 SSLとは直接関係なし。
- SSLEngine on
- SSLエンジンを有効にする。 あ、このへん書いとかないといけないかな。
- SSLProtocol all -SSLv2
- SSLのプロトコルの指定ですね。 「all」には SSLv2,SSLv3,TLSv1,TLSv1.1,TLSv1.2 が含まれているようで、そこから SSLv2 を除外する設定になっているようです。 情報を集めていると、SSLv3の脆弱性が報告されているので、無効化したほうがよい、という記事を見つけました。 ただ、クライアントの対応も必要になるので、あとで実験してみましょう。
- SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA
- SSLで暗号化通信に使う暗号アルゴリズム。 "!"のついているものは使用できないようにする。 ここも、通信の暗号化を強化するには編集が必要らしい。
- SSLCertificateFile /etc/pki/tls/certs/wiz-code.private.crt
- さっきコメントアウトしたところ。 電子証明書を指定する。
- SSLCertificateKeyFile /etc/pki/tls/private/wiz-code.private.key
- こちらもさっきコメントアウト。 電子証明書の秘密鍵を指定する。
- CustomLog logs/ssl_request_log
- カスタムログの保存先ディレクトリ。
結果発表

1時間経過...。 あれこれ手を加えていたら、出来ました。 いずれのドメインも、それぞれの電子証明書で閲覧できています。 (blog.wiz-code.privateの方は、自己署名証明書に関する警告が発せられましたが、例外を許可しました。)
で、何をしたかというと、ssl.confの「Listen 443」の下に、次の記述を加えました。
Listen 443 https
NameVirtualHost *:443
また、<VirtualHost>の手前に、次の記述を加えました。
<VirtualHost *:443> ServerName wiz-code.private DocumentRoot /var/www/wiz-code.private SSLEngine on SSLProtocol all -SSLv2 -SSLv3 SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA SSLCertificateFile /etc/pki/tls/certs/wiz-code.private.crt SSLCertificateKeyFile /etc/pki/tls/private/wiz-code.private.key </VirtualHost> <VirtualHost *:443> ServerName blog.wiz-code.private DocumentRoot /var/www/blog.wiz-code.private SSLEngine on SSLProtocol all -SSLv2 -SSLv3 SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA SSLCertificateFile /etc/pki/tls/certs/blog.wiz-code.private.crt SSLCertificateKeyFile /etc/pki/tls/private/blog.wiz-code.private.key </VirtualHost>
このうち、「NameVirtualHost」「SSLEngine」「SSLProtocol」「SSLCipherSuite」の3つについて、どこまで必須項目であるか、を検証します。 1行ずつコメントアウトし、Apacheを再起動してチェックした結果がこちら。
- NameVirtualHost
- 接続成功
- SSLEngine on
- 接続不能
- SSLProtocol
- 接続成功
- SSLCipherSuite
- 接続成功
このように、必須項目は「SSLEngine on」のみであることが分かりました。
SNIを利用して複数の電子証明書を使い分けたSSL接続はできるようになりましたが、いまひとつ設定の記述法を飲み込み切れていません。 セキュリティに大きく関わるところなので、時間を置いて改めて学習しようと思います。
この章のまとめ
- SNIを利用するときは、443ポートでVirtualHostディレクティブを使用する
- 初期の状態では、脆弱性が指摘されているプロトコルのバージョンが利用できるようになっている