Apache 2.4系でHTTP/2サーバを構築してみるテスト。
HTTP/2の各種設定続いて、HTTP/2のサーバとしての設定を行っていきたいと思います。 ここまでのインストール作業で、HTTP/1.1のサーバとして機能しているはずです。
既にApache2.4をインストールしている方
当サイトのインストール方法以外の方法でインストールした方は、nghttp2ライブラリを含んでいるか確認してください。 # ls /usr/local/apache2/modules/mod_http2.so /usr/local/apache2/modules/mod_http2.so # ls /usr/local/lib/libnghttp2.so /usr/local/lib/libnghttp2.so と出ていれば、OKのはずです。 これらのファイルが無い場合、Apache 2.4をコンパイルする前に、あらかじめ、nghttp2をコンパイルしておく必要もあります。 まだの人は以下のページを、ご覧ください。
また、HTTP/2はTLS1.2を前提としていますので、サーバ証明書をご用意ください。
httpd.confの設定まず、httpd.confを編集して、HTTP/2モジュール(http2_module)を読み込むように設定します。 HTTP/2モジュール(http2_module)がロードされた場合、プロトコル合意順序に関する設定などがなされるようにします。 # vi /usr/local/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
…(省略)… LoadModule http2_module modules/mod_http2.so <IfModule http2_module> LogLevel http2:info ProtocolsHonorOrder On Protocols h2c http/1.1 H2Direct on </IfModule> …(省略)… 上記設定は、ログレベルはhttp2:info、プロトコル合意順序(ProtocolsHonorOrder)を有効とし、その順序(Protocols)は、h2cとhttp/1.1の順序としています。また、h2cはダイレクトモードに対応するようになります。 これは、80番ポートを利用する、全バーチャルホスト共通の設定となります。 実際のWebブラウザは、80番ポートを利用する非暗号接続の「h2c」に対応していない可能性が高いです。構築時の参考程度に考えておくといいかもしれません。 負荷分散装置といった、リバースProxyの後方で運用するWebサーバで、利用するといった用途が、メインかと思われます。 h2cを使わないなら、動作検証が終わったら,80番ポート側のh2cは削っても良いです。 続いて、TLS(SSL)側の設定も行います。 まず、443ポートのデフォルトサーバの設定 # vi /usr/local/apache2/conf/extra/httpd-ssl.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
…(省略)… <VirtualHost _default_:443> SSLEngine on <IfModule http2_module> ProtocolsHonorOrder On Protocols h2 http/1.1 </IfModule> …(省略)… </VirtualHost> そして、、443ポートのバーチャルホストの設定にも同様に設定します。 443ポートの向けのバーチャルホストが複数ある場合は、すべてに設定します。 # vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
<VirtualHost *:443> SSLEngine on <IfModule http2_module> ProtocolsHonorOrder On Protocols h2 http/1.1 </IfModule> DocumentRoot "/var/https/example.com/htdocs/" …(省略)… </VirtualHost> こちらも、HTTP/2モジュールがロードされた場合、プロトコル合意順序を有効とし、その順序は、h2とhttp/1.1の順序としています。 TLS1.2のALPN拡張を利用するので、ダイレクトモードの設定などは必要ありません。(というかできません。) mod_http2のディレクティブ
ここでは、最低限の設定しか紹介していませんが、HTTP/2の細かな設定を行うディレクティブが用意されています。 下記にまとめてありますので、参考にしてください。 [参考]>>mod_http2のリファレンス
TLS接続HTTP/2のTLS接続は、Forward Secrecyであることが求められます。 Forward Secrecyとは、1つのTLS接続のセッションキーが解読されても、それ以外のTLS接続のセッションキーの解読につながらない状況を言います。 これを実現する暗号プロトコルはECDHEや、DHEと呼ばれる鍵交換を行う必要があります。それ以外は、Forward Secrecyを実現できません。 HTTP/2では、当然このForward Secrecyを目指しています。 そこで、HTTP/2のTLS接続(h2)では、RFC7540の末尾にあるブラックリストに掲載の暗号スイートをつかった場合、接続のエンドポイント(サーバやクライアント)は、接続エラーとして扱ってもよいとなっています。このブラックリストに載っている暗号スイートは、ECDHEとDHE以外の鍵交換方法を使った暗号スイートです。 HTTP/2に対応している世代のWebブラウザは、軒並みECDHEによる鍵交換で、AES128/256と、SHA256以上を組み合わせた暗号に対応しています。このように、すでにWebブラウザ側は準備万端ですので、どちらかといえば、サーバ側に、ForwardSecurityを強制する圧力となります。ブラックリストに載っている暗号スイートを使用した場合、いずれ、ブラウザ側がから接続エラーと認識されて、接続できないサーバになってしまう可能性があります。 手っ取り早く設定したい場合、おすすめのSSL設定は以下の通りです。 443ポートのデフォルトVirtualHost内に設定します。独断と偏見ですが、暗号スイートは強い順に並べたつもりです。 HTTP/1.1でも、Forward Secrecyを実現することはむしろ好ましいのです。この設定は、HTTP/1.1のサーバでも有用です。 # vi /usr/local/apache2/conf/extra/httpd-ssl.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
…(省略)… SSLProtocol -All +TLSv1 +TLSv1.1 +TLSv1.2 SSLHonorCipherOrder on SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-SHA256:DHE-DSS-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK …(省略)… HTTP/2だけを考えれば、SSLProtocolは、TLS1.2だけでいいのですが、
[2017年2月24日追記]
が見つかったため、SHA-1を含む暗号スイートの使用を停止することを推奨します。 ただし、Android4.3以前、Safari6以前を搭載した端末のうち、アクセスできなくなる可能性があります。 当然、SSLHonorCipherOrder で、暗号スイートの合意順序を有効にして、SSLCipherSuiteに、サーバとして使いたい暗号スイートを順番に記述します。 暗号スイートとは、鍵交換、鍵認証、共通鍵暗号、メッセージ署名方法といった、暗号アルゴリズム組み合わせです。 たとえば、ECDHE-ECDSA-AES256-GCM-SHA384は、鍵交換にECDHE、鍵認証にECSA、共通鍵暗号に256ビットのAES256-GCM、メッセージ署名に384ビットのSHA386を使う方法という意味になります。 TLSは、このように、鍵交換、鍵認証、共通鍵暗号、メッセージ署名方法を組み合わせて、安全なTLS接続を実現します。 暗号スイートは、強い順でも良いのですが、応用編では、適切な強度と、サーバ負荷を考えて設定する方法を紹介しています。 上記の設定には、鍵認証のアルゴリズムとして、ECDSAとか、DSSとか入っていますが、サーバ(SSL)証明書のデジタル署名がRSA方式だと、鍵認証としてRSAしか使われません。 DSSはともかく、ECDSAの証明書はなかなか高価です。
[2016年2月11日追記]
高価でしたが、過去形で語る時代となりました。ECDSA対応の証明書もLet's Encryptから無償で入手できるようになりました。 暗号鍵のビット数にもよりますが、ECDSA対応の証明書(256ビット)と、ほぼ同じ強度のRSAの証明書(3072ビット)を比べると、 サーバ側の処理が軽くなります。 一通り、設定が済んだら是非チャレンジしてみてください。 ガラケー自体は、HTTP/2に対応することはないと思いますが、ガラケー用HTTPSサーバとの共存が必要な場合もあると思います。 おすすめしませんが、もし、HTTP/2サーバのHTTP/1.1互換機能で、ガラケーのHTTPS通信に対応する場合、ガラケー用の暗号スイートを入れるとすれば、"!aNULL"の手前に入れてください。 ちなみに、手元のサーバ証明書で確認した範囲では、以下の暗号スイートが使われました。 すくなくとも、各OS/ブラウザは、表2にある暗号スイートを利用できる、といった程度の、参考にしてください。
ところで、SHA1(表2で、SHAの後に256や384といった数値がついていないもの)には、セキュリティー上の問題があるので、サーバ証明書については、SHA1の利用が収束してきています。 この流れは、サーバ証明書以外にも及んでくると思われますので、SHA1を含む暗号スイートしか使えないOS/ブラウザは、サービスの対象から徐々に外していく検討が必要でしょう。 表2にある通り、HTTP/2を使うブラウザは、軒並み、SHA256以上のハッシュ関数を使っていますから、この問題の影響はなさそうです。 なお、apple tvのiOS 9はSHA1の署名だと、接続を受け付けないようです。なお、無償で取得できる、Let's Encryptのサーバ証明書は、SHA256の署名なので、大丈夫です。(逆に、SHA256の署名だと、古いガラケーに対応していないですが…)
OCSPステープリングOCSP(Online Certificate Status Protocol)は、TLS接続を開始する際に、サーバ証明書が失効していないかを、確認するプロトコルです。 従来の証明書失効リスト(CRL)は、全リストをダウンロードしますが、OCSPは、単一サーバの証明書についてのみダウンロードすることができるため、性能的に有利です。 ただ、OCSPも、ブラウザがOCSPサーバ(OCSP Responder)に確認のための接続を行うため、その間、どうしても時間がかかってしまいます。これをHTTPSサーバ側でキャッシュして、TLS接続開始の際に、ブラウザに提供することで、接続時間の短縮を目指したのが、OCSPステープリングです。 OCSPステープリングには、Apache HTTPD Serverだと、2.3.3以降で対応しています。HTTP/2に対応している、2.4.17以降は当然対応しています。 # mkdir /var/run/ocsp # vi /usr/local/apache2/conf/extra/httpd-ssl.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
…(省略)… SSLProtocol -All +TLSv1 +TLSv1.1 +TLSv1.2 SSLHonorCipherOrder on SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-SHA256:DHE-DSS-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK SSLUseStapling on SSLStaplingResponderTimeout 5 SSLStaplingReturnResponderErrors off SSLStaplingCache shmcb:/var/run/ocsp(128000) …(省略)… apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように # /usr/local/apache2/bin/apachctl -t Syntax OK # /usr/local/apache2/bin/apachctl stop # /usr/local/apache2/bin/apachctl start 再起動します。 OCSPステープリングが有効かどうかは、opensslのコマンドから調べることができます。以下は、有効な場合です。 # openssl s_client -connect example.com:443 -status …(省略)… OCSP response: ====================================== OCSP Response Data: OCSP Response Status: successful (0x0) …(省略)… OCSPステープリングが無効であれば、以下のようになります。 # openssl s_client -connect example.com:443 -status …(省略)… OCSP response: no response sent …(省略)…
アクセスログの取り方従来のHTTP/1.1のサーバでは、
/usr/local/apache2/conf/extra/httpd-ssl-vhost.confの一部
<VirtualHost *:443> …(省略)… CustomLog "|/usr/local/apache2/bin/rotatelogs -l /var/log/https/example.com/access_log.%Y%m%d 86400" combined CustomLog "|/usr/local/apache2/bin/rotatelogs -l /var/log/https/example.com/ssl_request_log.%Y%m%d 86400" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" …(省略)… </VirtualHost> といったフォーマットでログをとっている人が多いと思います。(このまでの説明でも上記のログ設定となっています。) ところが、これではHTTP/2でアクセスしているにもかかわらず、 127.0.0.1 - - [05/Nov/2015:10:02:16 +0900] "GET / HTTP/1.1" 200 11170 "-" "Mozilla/5.0"
上記の現象は、Apache2.4.18で解消しました。ちゃんと、combinedでも、HTTP/2と出ます。 もちろん、 %H にすれば、ちゃんとHTTP/2と表示されます。 どうせ、ssl_request_logもとるんだったら一緒にしてしまえということで、私は、以下のログフォーマットを使ってます。 Apache 2.4.18以前
Apache 2.4.20以降
このログフォーマットの特徴は、
といったところです。 # vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
<VirtualHost *:443> …(省略)… CustomLog "|/usr/local/apache2/bin/rotatelogs -l /var/log/https/example.com/access_tls_log.%Y%m%d 86400" "%h \"%{%F %T %z}t\" %{SSL_PROTOCOL}x %{SSL_CIPHER}x %{pid}P %{tid}P %u %m \"%U\" \"%q\" %H %>s %B \"%{Referer}i\" \"%{User-agent}i\"" …(省略)… </VirtualHost> 次のコマンドでApacheのリスタート # /usr/local/apache2/bin/apachctl -t Syntax OK # /usr/local/apache2/bin/apachctl stop # /usr/local/apache2/bin/apachctl start 続く、HTTP/2の動作確認で、HTTP/2のアクセスを行うと、以下のように、HTTP/2と出力されます。 127.0.0.1 "2016-04-20 14:12:14 +0900" TLSv1.2 ECDHE-RSA-AES128-GCM-SHA256 24405 18-3 - GET "/css/common.css" "" HTTP/2 200 1275 "https://example.com/" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36" "PUSHED" 1 完全に、好みの問題ですが、HTTP/2のログをどうしていいかわからない人におすすめです。 HTTP/2は人間からのアクセス?!ちなみに、HTTP/2に対応してみるとよくわかるのですが、ログを見ると、人間が(ブラウザを使って)アクセスしているとHTTP/2、ロボットだとHTTP/1.1になります。 Chrome, Firefox, Internet Explorer, Microsoft Edgeいずれも、HTTP/2(h2)に対応していて、これが支配的なんですね。 ロボットもTLS1.2のALPNを設定して、次第に対応してくると思いますが、それはまだ先のことのようです。
HTTP/2へのリダイレクトh2のサーバが安定したら、80番ポート側のバーチャルホストは、閉じたくなるのが人情。 しかし、しばらくは、検索エンジンからのアクセスも、80番経由となります。 また、ほとんどのブラウザは、優先して80番ポートに接続に来ます。これは当面、このままでしょう。 そこで、HTTP/2のサーバにリダイレクトするように設定します。ブラウザや、検索エンジンからのアクセスが、直接、HTTP/2経由になるまで待ちましょう。 # vi /usr/local/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
…(省略)… LoadModule rewrite_module modules/mod_rewrite.so ←コメント解除 …(省略)… 次に、80番ポートのHTTPのバーチャルホストを設定します。 # vi /usr/local/apache2/conf/extra/httpd-vhosts.conf
/usr/local/apache2/conf/extra/httpd-vhosts.conf
…(省略)… <VirtualHost *:80> …(省略)… <IfModule mod_rewrite.c> RewriteEngine on RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] </IfModule> …(省略)… </VirtualHost> …(省略)… リダイレクトのレスポンスステータスコードは、301となるようにR=301にします。これで、検索エンジンにも、恒久的な移動であることを知らせます。 apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように # /usr/local/apache2/bin/apachctl -t Syntax OK # /usr/local/apache2/bin/apachctl stop # /usr/local/apache2/bin/apachctl start 再起動します。これで、80番ポートに接続しても443番ポートに飛びます。
Let's Encryptのリダイレクト対応Let's Encryptのサーバはドメイン認証の際に、HTTP/1.1⇒HTTPSのリダイレクトを経て、HTTPS側の /.well-known/acme-challenge/ にある認証用のファイルをとってきてくれます。 そのため非暗号のHTTPに来たリクエストは、丸ごと、HTTPSに転送するようにmod_rewriteを設定してもいいのですが、HTTPSに認証用のフォルダを作成したくない時もあります。 そこで、ACME認証のみHTTPで処理し、それ以外をHTTPSに転送する設定を紹介します。 # vi /usr/local/apache2/conf/extra/httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf
…(省略)… <VirtualHost *:80> …(省略)… <IfModule mod_rewrite.c> RewriteEngine on RewriteCond %{HTTPS} off RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] </IfModule> …(省略)… </VirtualHost> …(省略)… apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように # /usr/local/apache2/bin/apachctl -t Syntax OK # /usr/local/apache2/bin/apachctl stop # /usr/local/apache2/bin/apachctl start 再起動します。これで、「ACMEプロトコルにおけるドメイン認証」、以外のリクエストは、80番ポートから443番ポートに飛びます。
2018年1月10日に、Let's Encryptのドメイン認証方法のひとつである、TLS-SNIチャレンジ(tls-sni-01)に脆弱性が発見されたため、以下の方法は推奨できません。
前述の通り、80番ポートから、443番ポートのHTTPS(HTTP/2)へ、リダイレクトを行うと、非暗号(80番ポート)のHTTP/1.1のhtdocs配下は、閲覧できなくなります。 つまり、Let's Encrypt(ACMEプロトコル)が利用していた、80番ポートのバーチャルホストのhtdocs配下にコンテンツを置いても、閲覧できません。ここに、「認証ためのファイル」を設置しても、ACMEサーバから、見られなくなります。 当然、Let's EncryptのACMEサーバからの認証チャレンジが、失敗してしまいます。 そこで認証用ファイルを、HTTPS側に設置するようにします。具体的には、letsencrypt-autoの--webrootオプションを、HTTPS通信用の/var/https/example.com/htdocs/に変更します。 さらにACMEサーバに、HTTP/1.1からリダイレクトを実行するように、--rediretオプションを追加します。 仕上げに、Renewal用のファイルを更新するため、一回、以下のコマンドをコマンドラインからたたきます。 $ cd /usr/local/letsencrypt $ ./letsencrypt-auto certonly -t -d example.com -a webroot --webroot-path=/var/https/example.com/htdocs/ --rsa-key-size 2048 --server https://acme-v01.api.letsencrypt.org/directory --rediret 同じく、自動取得用のスクリプトも書き換えます。 # vi /usr/local/apache2/bin/cert_refresh.sh
/usr/local/apache2/bin/cert_refresh.sh
#! /bin/sh cd /usr/local/letsencrypt ./letsencrypt-auto certonly -t -d example.com -a webroot --webroot-path=/var/https/example.com/htdocs/ --renew-by-default --server https://acme-v01.api.letsencrypt.org/directory --redirect /usr/local/apache2/bin/apachectl restart これで、リダイレクトして、認証してくれます。 ちなみに、HTTP/1.1でアクセスしてきますので、HTTP/1.1でもアクセスできるようにしておく必要があります。つまり、「Protocols h2 http/1.1」といった設定が、不可欠です。h2だけで運用するのは、もう少し後になりそうです。設定的には、以下のようになっているか確認してください。 # vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl.confの一部
<VirtualHost *:443> SSLEngine on <IfModule http2_module> ProtocolsHonorOrder On Protocols h2 http/1.1 </IfModule> DocumentRoot "/var/https/example.com/htdocs/" …(省略)… </VirtualHost>
HTTP Strict Transport Security(HSTS)リダイレクトの設定をしたらなら、次に、HTTP側からHTTPS側にリダイレクトされた際に、ブラウザに次回以降、直接HTTPSにアクセスするよう、覚えてもらいます。 これをHTTP Strict Transport Security(HSTS)と呼びます。これは、リダイレクトの際に、HTTP→HTTPSと通信が切り替わるまでの間に、中間者攻撃が行われる可能性があるため、これを防ぐため、ブラウザに直接HTTPSに来てもらうように覚えてもらう仕組みです。有効期限(max-age:秒単位)と一緒に設定します。 これは、HTTPS(TLS)側のバーチャルサーバに置きます。 以下の設定では、今後180日、当該バーチャルサーバのドメインに対して、HTTPではなく、HTTPSに直接来てもらうことを覚えてもらいます。 つまり、最短で半年に1回は、HTTPへアクセスしてきますので、この設定は、リダイレクト設定とセットで、使い続けます。 まず、httpd.confを編集して、ヘッダモジュール(headers_module)を読み込むように設定します。 # vi /usr/local/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
…(省略)… LoadModule headers_module modules/mod_headers.so …(省略)… 続いて、次回は直接443ポートに接続するようヘッダを追加します。 # vi /usr/local/apache2/conf/extra/httpd-ssl-vhost.conf
/usr/local/apache2/conf/extra/httpd-ssl-vhost.confの一部
<VirtualHost *:443> …(省略)… Header add Strict-Transport-Security "max-age=15552000" …(省略)… </VirtualHost> apachctl -tで、設定ファイルの間違いが無いことを確認の上、下記のように # /usr/local/apache2/bin/apachctl -t Syntax OK # /usr/local/apache2/bin/apachctl stop # /usr/local/apache2/bin/apachctl start 再起動します。これで、中間者攻撃のリスクが減りました。 無いとは思いますが、一旦、上記設定を反映後、HTTPからのリダイレクトを止めて、HTTPSとHTTPで異なるコンテンツを展開したい場合は、DNSのTTLを短縮するのと同様、事前に、max-ageの短縮を行わなければなりません。180日であれば、180日前から、短縮の必要があります。
|