Posted at

[IPv6対応] nginxとLet's encryptでA+級のセキュアなWebサーバー構築

More than 3 years have passed since last update.


はじめに

nginx実践入門:書籍案内|技術評論社 - Gihyo.jpを参考にして、nginxとLet's encryptでセキュアなWebサーバーを構築したのでメモします。もちろん、SSL Server Test (Powered by Qualys SSL Labs)の評価がGrade A+となるようにサーバーを構築します。ついでにHTTP/2とIPv6にも対応させました。


環境について

この記事で説明している内容は次の環境でテストを行いました。


  • OS: Ubuntu 15.10

  • サーバー: さくらのVPS 1コア/メモリ512MB

また、この記事で使用しているソフトウェアのバージョンは次のとおりです。


  • letsencrypt-0.5.0

  • nginx-1.9.14

  • pcre-8.38

  • zlib-1.2.8

  • openssl-1.0.2g

この記事ではドメインsakura.mouten.infoを例に説明を進めていきます。ドメインは各々の環境に合わせて読み替えてください。


Let's encryptのセットアップ

Let's encryptを利用する方法はいくつかあるのですが、この記事ではLet's encryptが提供している公式クライアントを利用することにします。


Let's encryptクライアントのインストール

それでは、Getting Started - Let's Encrypt - Free SSL/TLS Certificatesに従ってLet's encryptの公式クライアントであるletsencrypt-autoをインストールします。

$ cd /usr/local/

$ sudo git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ . sudo /letsencrypt-auto --help

letsencrypt-autoを初めて実行したときには必要なパッケージのインストールやletsencrypt-auto自身のアップデートが行われます。最終的にヘルプが表示されればクライアントのインストールは完了です。


DNSのAレコードとAAAAレコードの確認

さて、クライアントがインストール出来たので早速証明書を発行したいところですが、その前にDNSが正しく設定されているかを確認しておく必要があります。第三者が証明書を発行するのを防ぐため、letsencrypt-autoを実行したサーバーのIPアドレスとDNSのAレコードまたはAAAAレコードに設定されているIPアドレスが一致しない場合、証明書の発行はできません。まずはdigコマンドでAレコードとAAAAレコードに設定されているIPアドレスを確認します。

$ dig sakura.mouten.info. any | grep -v '^;\|^$'

sakura.mouten.info. 1200 IN AAAA 2001:e42:102:1821:160:16:238:242
sakura.mouten.info. 1200 IN A 160.16.238.242

続いて、サーバーのネットワークインターフェースに割り当てられているIPアドレスをipコマンドで確認します。ここでは例として、eth0にIPアドレスが割り当てられている場合の結果を示します。

$ ip addr show eth0 | grep inet

inet 160.16.238.242/23 brd 160.16.239.255 scope global eth0
inet6 2001:e42:102:1821:160:16:238:242/64 scope global
inet6 fe80::9ea3:baff:fe02:23ce/64 scope link

DNSのAレコードとAAAAレコードに設定されているIPアドレスがネットワークインターフェースに設定されているIPアドレスと一致していることが確認できました。


証明書の発行

準備ができたので、証明書を発行します。次のコマンドを実行してください。

$ sudo ./letsencrypt-auto certonly --standalone -d sakura.mouten.info

上記のコマンドを実行すると、まずメールアドレスの入力が求められます。その後Let's encryptのりようきアクに同意するか尋ねられますので、同意する場合はagreeを選択してください。無事に証明書が発行されると、次のようなメッセージが表示されます。

...

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/sakura.mouten.info/fullchain.pem. Your cert
will expire on 2016-07-20. To obtain a new version of the
certificate in the future, simply run Let's Encrypt again.
- If you lose your account credentials, you can recover through
e-mails sent to moutend@gmail.com.
- Your account credentials have been saved in your Let'
s Encrypt
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Let's
Encrypt so making regular backups of this folder is ideal.
- If you like Let'
s Encrypt, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

証明書が発行出来たら、上記メッセージにも書かれているとおり、/et/letsencryptのバックアップを作成しておくことをおすすめします。


証明書を自動更新するための設定

Let's encryptから発行された証明書の有効期間は90日です。有効期限の30日前になると証明書の延長ができるようになるので、letsencrypt-auto renewを実行して、有効期間を延長します。なお、有効期間が残っていて、延長の必要が無い場合はletsencrypt-auto renewを実行しても証明書は更新されません。詳細についてはGetting Started - Let's Encrypt - Free SSL/TLS Certificatesを確認してください。

さて、証明書を更新すると有効期間が90日間延長されるため、約1年間で4回更新することになります。それくらいの頻度であれば手動で証明書を更新できそうだと思われるかもしれませんが、証明書の自動更新をするsystemdサービスを追加することにします。


自動更新の頻度について

有効期間が90日間ということで、証明書の自動更新は90日に1回行われるように設定すればOKかといえば、そうではありません。Let's encryptのチュートリアルには次のように書かれています。


Once you’re happy with your script, you can run it with cron or systemd. We recommend running renewal scripts at least daily, at a random hour and minute. This gives the script many days to retry renewal in case of transient network failures or server outages.


少なくとも毎日(12:00など決まった時刻ではなくランダムな時刻に)証明書の更新用のスクリプトを実行することが勧められています。毎日というのは過剰な気もしますが、サーバーが停電するなどの障害が発生して証明書が更新されなかった場合に、証明書を更新するための猶予期間が確保できるので、これくらいが更新の頻度として適切ではないかと思います。


証明書を更新するためのsystemdサービスを作成

それでは、/etc/systemd/systemディレクトリに次のファイルを作成してください。


  • letsencrypt-renew.timer

[Unit]

Description=Run `letsencrypt-auto renew ` every day

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target


  • letsencrypt-renew.service

[Unit]

Description=Renew all certificates if they are about to expire

[Service]
Type=simple
ExecStart=/bin/bash -c "sleep `expr $RANDOM % 3600`; /usr/local/letsencrypt/letsencrypt-auto renew --non-interactive"

[Install]
WantedBy=multi-user.target

上記のletsencrypt-renew.serviceは、0から3600秒のランダムな時間スリープした後にletsencrypt-auto renewを実行するというものです。これをletsencrypt-renew.timerで毎日実行させることで、深夜0:00から1:00の間にletsencrypt-auto renewを実行するようにしています。それでは、次のコマンドを実行してサービスを起動します。

# サービスを有効にする

$ sudo systemctl enable letsencrypt-renew.timer
$ sudo systemctl enable letsencrypt-renew
# サービスを起動する
$ sudo systemctl start letsencrypt-renew.timer
$ sudo systemctl start letsencrypt-renew

これで証明書が自動で更新されるようになりました。


証明書が更新されたかを確認

証明書が更新されたかを確認したい場合は、journalctlコマンドで確認することができます。

$ journalctl -f -u letsencrypt-renew

証明書の有効期間が残っている状態でletsencrypt-renew.serviceが実行されて証明書の延長が試みられると、例えば次のようなログが記録されます。

$ journalctl -f -u letsencrypt-renew

-- Logs begin at Wed 2016-04-27 07:49:43 JST. --
Apr 27 08:08:23 tk2-261-40238 systemd[1]: Started Renew all certificates if they are about to expire.
Apr 27 08:51:21 tk2-261-40238 bash[1019]: Checking for new version...
Apr 27 08:51:22 tk2-261-40238 bash[1019]: Requesting root privileges to run letsencrypt...
Apr 27 08:51:22 tk2-261-40238 bash[1019]: ~/.local/share/letsencrypt/bin/letsencrypt renew --non-interactive
Apr 27 08:51:22 tk2-261-40238 bash[1019]: -------------------------------------------------------------------------------
Apr 27 08:51:22 tk2-261-40238 bash[1019]: Processing /etc/letsencrypt/renewal/sakura.mouten.info.conf
Apr 27 08:51:22 tk2-261-40238 bash[1019]: -------------------------------------------------------------------------------
Apr 27 08:51:22 tk2-261-40238 bash[1019]: The following certs are not due for renewal yet:
Apr 27 08:51:22 tk2-261-40238 bash[1019]: /etc/letsencrypt/live/sakura.mouten.info/fullchain.pem (skipped)
Apr 27 08:51:22 tk2-261-40238 bash[1019]: No renewals were attempted.


DHパラメータの生成

PFSのためのDHパラメータを生成します。すでにご存知かと思いますが、PFSの概要をnginx実践入門から引用します。


近年重要視されている暗号化通信の機能にPFSと呼ばれるものがあります。これまでのすべての暗号化された通信を記録しておき、あとからある期間の鍵を盗むことができたとしても一定期間の通信しか復号できず、それ以前またはそれ以後に記録した通信は復号できない暗号化通信の性質を指します。鍵交換方式としてECDHE、またはDHEを使用する暗号化スイートではこのPFSの条件を満たしています。DHEではdhparamと呼ばれるDHパラメータ()を使用しますが、このパラメータに使用する鍵長は2,048ビット以上にすることが推奨されています。dhparamはopensslコマンドで鍵長を指定して生成できます。


それでは、/etc/nginx/sslにdh2048.pemというDHパラメータファイルを作成します。

$ sudo mkdir -p /etc/nginx/ssl

$ sudo openssl dhparam 2048 -out /etc/nginx/ssl/dh2048.pem

Let's encryptの設定はこれで完了です。


nginxのセットアップ

証明書が発行されたので、次はnginxの設定を行います。nginxはaptなどのパッケージマネージャーでインストールしてもいいのですが、この記事ではソースコードからビルドします。


nginxのビルドに必要なライブラリのインストール

次の内容をinstall.shという名前で保存してください。

#!/bin/bash

set -e
mkdir ~/tmp
cd ~/tmp

# opensslソースコードのダウンロード
openssl_version='openssl-1.0.2g'
wget ftp.openssl.org/source/$openssl_version.tar.gz
tar xzf $openssl_version.tar.gz
ln -s $openssl_version openssl

# zlibソースコードのダウンロード
zlib_version='zlib-1.2.8'
wget http://zlib.net/$zlib_version.tar.gz
tar xzf $zlib_version.tar.gz
ln -s $zlib_version zlib

# pcreソースコードのダウンロード
pcre_version='pcre-8.38'
wget http://ftp.csx.cam.ac.uk/pub/software/programming/pcre/$pcre_version.tar.gz
tar xzf $pcre_version.tar.gz
ln -s $pcre_version pcre

# nginxソースコードのダウンロード
nginx_version='nginx-1.9.14'
wget http://nginx.org/download/$nginx_version.tar.gz
tar xzf $nginx_version.tar.gz
ln -s $nginx_version nginx

install.shを実行してnginxのビルドに必要なソースコードをインストールします。

$ bash install.sh

これでnginxのビルドに必要なソースコードが揃いました。


nginxのビルド

それでは、nginxをビルドします。まずは、次のようなオプションを渡して./configureを実行します。

$ cd ~/tmp/nginx

$ ./configure \
--with-ipv6 \
--with-http_v2_module \
--sbin-path=/usr/local/sbin/nginx \
--with-pcre=$HOME/tmp/pcre --with-pcre-jit \
--with-zlib=$HOME/tmp/zlib \
--with-openssl=$HOME/tmp/openssl --with-http_ssl_module

最後に、makeしてビルドは完了です。

$ make

$ sudo make install

念のためnginx -Vと入力して、オプションが正しく渡されたうえでビルドが行われたか確認します。

$ nginx -V

nginx version: nginx/1.9.14
built by gcc 5.2.1 20151010 (Ubuntu 5.2.1-22ubuntu2)
built with OpenSSL 1.0.2g 1 Mar 2016
TLS SNI support enabled
configure arguments: --with-ipv6 --with-http_v2_module --sbin-path=/usr/local/sbin/nginx --with-pcre=/home/moutend/tmp/pcre --with-pcre-jit --with-zlib=/home/moutend/tmp/zlib --with-openssl=/home/moutend/tmp/openssl --with-http_ssl_module

上記のconfigure arguments: 以降が./configureに渡したオプションと一致していれば、ビルドは成功です。


nginx.confの設定

それでは/usr/local/nginx/conf/nginx.confを設定します。内容は次のとおりです。

worker_processes 1;

events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# ヘッダを追加してHSTSを有効にする。
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains;';
# gzip圧縮の設定(圧縮対象のフォーマットや圧縮レベルなどは各々の環境に合わせて設定してください。)
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml text/javascript;
gzip_comp_level 6;
server {
server_name sakura.mouten.info;
# IPv4とIPv6の両方でHTTPリクエストを受け付ける。
listen *:80;
listen [::]:80;
# HTTPからのアクセスをHTTPS減りダイレクトする。
return 301 https://$host$request_uri;
location / {
root html;
index index.html;
}
}
server {
server_name sakura.mouten.info;
# IPv4とIPv6の両方でHTTPS(v1とv2)のリクエストを受け付ける。
listen *:443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# サーバー側で指定する暗号化スイートリストの選択を優先する。
ssl_prefer_server_ciphers on;
# 暗号化スイートのリストを指定する。
# ここではModernを選択していますが、暗号化スイートリストの選択方法については、次のドキュメントを参照してください。
# Security/Server Side TLS - MozillaWiki
# https://wiki.mozilla.org/Security/Server_Side_TLS
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
# Let's encryptで発行した証明書と秘密鍵を指定する。
ssl_certificate /etc/letsencrypt/live/sakura.mouten.info/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sakura.mouten.info/privkey.pem;
# DHパラメータを指定する。
ssl_dhparam /etc/nginx/ssl/dh2048.pem;
# 次回のTLSハンドシェイクをスキップするため、セッションキャッシュを有効にする。
ssl_session_cache shared:SSL:10m;
# OCSPステープリングを有効にする。
ssl_trusted_certificate /etc/letsencrypt/live/sakura.mouten.info/chain.pem;
ssl_stapling on;
ssl_stapling_verify on;
# OCSPレスポンダの名前解決に使用するネームサーバーを指定する(省略してもOKです。)
resolver 210.188.224.10 210.188.224.11;
location / {
root html;
index index.html;
}
}
}


nginx.confのシンタックスを確認

nginx.confのシンタックスが正しいかチェックしたい場合は次のコマンドを実行します。

$ sudo nginx -t

シンタックスに誤りがなければ、次のように表示されます。

$ sudo nginx -t

nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

nginxのセットアップはこれで完了です。


ファイアウォールの確認

nginx.confの設定が終わったので早速起動してみたいところですが、起動する前にファイアウォールの設定を行っておく必要があります。この記事ではOSとしてUbuntuを使用しているため、ufwコマンドでファイアウォールの設定を行います。それでは、次のコマンドを実行してHTTPの80番ポートとHTTPSの443番ポートを許可するように設定してください。

# HTTPとHTTPSを許可

$ sudo ufw allow http
$ sudo ufw allow https
# ファイアウォールを有効にする
$ sudo ufw enable

ファイアウォールの設定は異常です。


テスト

次のコマンドを実行して、nginxを起動します。

$ sudo nginx


HTTPのリクエストをテスト

まずはHTTPのリクエストをテストします。curlコマンドでhttps://sakura.mouten.info/にアクセスしてみます。

$ curl -I https://sakura.mouten.info/

HTTP/1.1 200 OK
Server: nginx/1.9.14
Date: Tue, 26 Apr 2016 01:37:33 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 Apr 2016 01:28:22 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "571ec436-264"
Strict-Transport-Security: max-age=31536000; includeSubDomains;
Accept-Ranges: bytes

HTTPSでsakura.mouten.infoにアクセスできました。続いて、HTTPからのリクエストがHTTPSへリダイレクトされるかを確認します。

$ curl -I http://sakura.mouten.info/

HTTP/1.1 301 Moved Permanently
Server: nginx/1.9.14
Date: Tue, 26 Apr 2016 01:41:30 GMT
Content-Type: text/html
Content-Length: 185
Connection: keep-alive
Location: https://sakura.mouten.info/
Strict-Transport-Security: max-age=31536000; includeSubDomains;

ステータスコード301 Moved Permanentlyが返され、HTTPSにリダイレクトされていることが確認できました。


HTTP/2のリクエストをテスト

続いてHTTP/2でhttps://sakura.mouten.info/にアクセスできるかテストします。

$ curl --http2 -I https://sakura.mouten.info/

HTTP/2.0 200
server:nginx/1.9.14
date:Tue, 26 Apr 2016 03:54:04 GMT
content-type:text/html
content-length:612
last-modified:Tue, 26 Apr 2016 01:28:22 GMT
vary:Accept-Encoding
etag:"571ec436-264"
strict-transport-security:max-age=31536000; includeSubDomains;
accept-ranges:bytes

HTTP/2でリクエストが受け付けられていることが確認できました。


OCSPステープリングのテスト

OCSPステープリングが機能しているかはopensslコマンドで確認できます。次のようにコマンドを実行してください。

$ openssl s_client -connect sakura.mouten.info:443 -status

コマンドを実行すると次のように表示されます。

$ openssl s_client -connect sakura.mouten.info:443 -status

CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = sakura.mouten.info
verify return:1
OCSP response:
======================================
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
Produced At: Apr 25 01:24:00 2016 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D
Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1
Serial Number: 03DCB1BD6806D61A3CAA690CA3C39912C731
Cert Status: good
This Update: Apr 25 01:00:00 2016 GMT
Next Update: May 2 01:00:00 2016 GMT

Signature Algorithm: sha256WithRSAEncryption
...
---
Certificate chain
0 s:/CN=sakura.mouten.info
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
subject=/CN=sakura.mouten.info
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 3693 bytes and written 450 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Session-ID: FDFEBC5FE4FD70391D390A524E110717D3C6B0E156C1587AF8D98E7825558A36
Session-ID-ctx:
Master-Key: C0AF6A9ED1329521E3461CC9219DA95FC573EE10018D3D4BC566A44DDC474DC31289357967B039CB8CCC2028D104C6B5
Key-Arg : None
PSK identity: None
...
Start Time: 1461635661
Timeout : 300 (sec)
Verify return code: 0 (ok)
---

read:errno=0

コマンドを実行した結果にOCSP Response Status: successfulというメッセージが含まれていれば、OCSPは機能していることが確認できます。また、OCSPの話からはそれますが、Cipher : ECDHE-RSA-AES256-GCM-SHA384というメッセージが含まれており、nginx.confのssl_ciphersで指定した暗号化スイートリストが選択されていることが確認できます。


Qualys SSL Labsでサーバーの評価を確認

ここまでの設定が正しく行われていれば、Qualys SSL LabsでA+の評価が与えられます。自分のWebサーバーのドメインを入力して確認してみてください。


おわりに

この記事で説明した内容をansibleで自動化すればセキュアなWebサーバーを手軽に作ったり壊したりできて便利です。ただし、Let's encryptの証明書は発行数に制限があり、週に5回までとなっているので注意してください。詳細についてはRate Limits for Let's Encrypt - Documentation - Let's Encrypt Community Supportを確認してください。

Enjoy!


参考文献


  1. nginx実践入門:書籍案内|技術評論社 - Gihyo.jp

  2. O'Reilly Japan - ハイパフォーマンス ブラウザネットワーキング

  3. Getting Started - Let's Encrypt - Free SSL/TLS Certificates

  4. Security/Server Side TLS - MozillaWiki

  5. Module ngx_http_ssl_module

  6. Module ngx_http_gzip_module

  7. Best Practices for Using the Vary Header - Fastly

  8. How To Secure Nginx with Let's Encrypt on Ubuntu 14.04 - DigitalOcean

  9. Nginx/ Configure and Install With IPv6 Networking Support

  10. Getting Started with systemd

  11. systemd/Timers - ArchWiki

  12. Performing Math Calculation in Bash - Shell Tips!