TL;DR
nginx は LibreSSL (OpenSSL) をソースから直接 static link できます。すなわち、システム全体の OpenSSL に影響を与えることなく、外部にポートをさらすことになる nginx だけ、安全な LibreSSL に移行することができます。
設定は簡単で nginx コンパイル時に --with-openssl
オプションで libressl のソース tarball を展開したディレクトリを指定するだけです。
$ ./configure --with-openssl=path/to/libressl-2.4.5
背景
脆弱性対策
OpenSSL の脆弱性がまた見つかりましたね。
今回の経緯はなかなか強烈で、2016/9/22 に CVE-2016-6304 が発表されたと思ったら、その修正で新しい脆弱性 CVE-2016-6309 を作ってしまったという、修正によるエンバグで、他人事とは思えず胸が痛くなってしまいます。
脆弱性の発表後、修正がディストリビューション提供バージョンにバックポートされるまでさらに時間がかかります。Amazon Linux は対応が早く発表同日に更新が提供されましたが、CentOS (Red Hat Enterprise Linux) への反映は5日後でした。
ディストリビューションに修正が反映されるのを待つのはかなりやきもきするものです。IT 系サイトにはすぐにニュースが掲載され、情報感度の高いお客様からはお問い合わせがあることも珍しくありません。ですが、パッケージに依存している場合、できることはディストリビューションの更新を待つことだけです。
直接 OpenSSL を最新ソースコードから利用していても、この頻度で問題が発生していては、管理サーバーが多くなるにつれて、対応に必要なリソースも馬鹿になりません。
この問題を受けて、OpenSSL から fork した LibreSSLはどうなのかなと調べてみたら、OpenSSL で見つかった脆弱性はほとんど修正済で影響を受けていないことがわかりました。特に今年に入ってからが顕著です。
LibreSSL はそもそも OpenSSL を根本的に書き直して潜在的な脆弱性を潰すための fork なので、新しく見つかった OpenSSL の脆弱性もほとんどが影響を受けないというのは当然といえば当然なのですが、一方で積極的な書き換えなので安定性はどうなんだろうなぁ、なんて思っていました。
ですが、あらためて調べてみて LibreSSL は OpenSSL を置き換えて問題ないレベルで安定していると判断して、今回の問題を機に乗り換えることにしました。冒頭の TL;DR でも軽く触れていますが、システム全体の OpenSSL はそのままで nginx だけに適用できるので、他への影響を考えることなく移行できるのも判断材料です。
Chrome で http2 使いたい
ディストリビューションの OpenSSL ではなく、ソースコードから直接 OpenSSL を使うきっかけは Chrome でした。ある日、developer tool で network タブを見ていると、http2 対応させたはずなのに、http/1.1 で通信していることに気付きました。
調べてみたら、Chrome 51 から negotiation protocol が NPN から ALPN に切り替えられたことがわかりました。記事中にもありますが、各 Linux ディストリビューションで採用されている OpenSSL のバージョンは 1.0.1 系で、ALPN に対応した 1.0.2 系を採用しているのは Ubuntu 16.04 以降のみという状況です。
distribution | openssl version |
---|---|
CentOS 5.x | 0.9.8e |
CentOS 6.x | 1.0.1e |
CentOS 7.x | 1.0.1f |
Amazon Linux 2016.09 | 1.0.1k |
Ubuntu 14.04 TLS | 1.0.1f |
Ubuntu 16.04 TLS | 1.0.2g |
幸いにして、nginx は --with-openssl
で OpenSSL を static link できることが上記記事であわせて対策として記述されています。Chrome 51 以降で http2 を有効にするために、nginx では OpenSSL の最新版をソースから利用することにしました。
LibreSSL も 2.1.3 から ALPN に対応しているので、OpenSSL から乗り換えても問題なく Chrome で http2 通信は有効になります。
モバイルで高速化
LibreSSL は新しい TLS の暗号方式 ChaCha20-Poly1305 に対応しています (OpenSSL も 1.1.0 以降で対応している)。
詳細はリンクした記事に譲りますが、ARM アーキテクチャを採用しているスマートフォンでは、Intel の AES-NI 命令のように、AES のエンコード・デコードにハードウェア補助を使えないため、AES の計算負荷が無視できません。
ARM 上では、ChaCha20-Poly1305 は AES-GCM より3倍近く高速に計算できるので、スマートフォンからの利用者が多い場合は、ChaCha20-Poly1305 を優先するよう指定しましょう。
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
業務系アプリのように PC での利用者に限られるのであれば、ハードウェアサポートのある AES-GCM の方が高速なので、cipherli.stに従うのがベストです。
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
設定方法
TL;DR の通りコンパイル時に --with-openssl
オプションで、libressl のソースを展開したディレクトリを指定するだけで OK。LibreSSL を自分でコンパイルしたりインストールしたりする必要はありません。
$ tar xfz libressl-2.4.5.tar.gz
$ tar xfz nginx-1.11.9.tar.gz
$ cd nginx-1.11.9
$ ./configure --prefix=/opt/nginx-1.11.9 \
--with-http_ssl_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-http_v2_module \
--with-http_realip_module \
--with-openssl=../libressl-2.4.5 \
&& make && sudo make install
nginx -V
でビルドされたバイナリを確認してみましょう。
$ /opt/nginx/sbin/nginx -V
nginx version: nginx/1.11.9
built by gcc 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC)
built with LibreSSL 2.4.5
TLS SNI support enabled
configure arguments: --prefix=/opt/nginx-1.11.9 --with-http_ssl_module --with-http_gzip_static_module --with-http_stub_status_module --with-openssl=/home/***/libressl-2.4.5 --with-http_v2_module --with-http_realip_module
built with LibreSSL 2.4.5 と確かに LibreSSL が利用されていることがわかります。あとは cipherli.st のベストプラクティスに従って nginx.conf を設定するだけです。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
# スマホ利用者の多いサイトでは ChaCha20-Poly1305 を優先
# ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver $DNS-IP-1 $DNS-IP-2 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
設定できたら ssllabs で、安全かどうかをチェックするのを忘れずに。
A+ と評価されることが確認できると思います。