wiz.code

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(wiz-code.private)の作成

次に、それぞれのディレクトリに「index.html」を作成し、ホスト名のみを表示します。 これは、VirtualHostがちゃんと動作しているか確認するためのものです。

# vi /var/www/wiz-code.private/index.html
<!DOCTYPE html>
<html>
<h1>wiz-code.private</h1>
</html>
index.html(blog.wiz-code.private)の作成
# vi /var/www/blog.wiz-code.private/index.html
<!DOCTYPE html>
<html>
<h1>blog.wiz-code.private</h1>
</html>
virtualhost.confの記述

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を再起動

最後に、Apacheを再起動させます。

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

ブラウザで接続確認

ブラウザでそれぞれのURLにアクセスすると、ちゃんとそれぞれのホスト名が表示されています。 ここまでは、Windows版のApacheでも設置したことがあるのでスムーズ。

SNIの設定

次に、SSLで接続しています。 まずは「https://wiz-code.private」に接続してみましょう。

HTTPSで接続

む…。 バーチャルホストが効いてない。

あ、そっか。 virtualhost.confで80番ポートしか指定してないから…。

virtualhost.confのそれぞれの要素に、443ポートも追加します。

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>
HTTPSで接続成功

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
https.serviceの状態確認

こんどは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
カスタムログの保存先ディレクトリ。

結果発表

SNIの接続成功

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接続はできるようになりましたが、いまひとつ設定の記述法を飲み込み切れていません。 セキュリティに大きく関わるところなので、時間を置いて改めて学習しようと思います。

この章のまとめ