SSL Server TestでA+とれたぜ。SPDYとHTTP/2にも対応した。そこそこ大変だったので困ったところなどをメモ。
概略
このサイトは
endaaman.me
... Reactを使ったSPAなフロントエンド。SSRも行うapi.endaaman.me
... koa.js + MongoDBなREST APIstatic.endaaman.me
... リアルタイムサムネイル生成機能付き静的ファイル配信サーバー
の3つのサービスに分かれており、それぞれ個別のドメインを持ち、異なるDockerコンテナ上で動いている。サーバーマシンのフロントエンド(WWWに繋がる部分)はjwilder/nginx-proxyだけを-p 80:80 -p 443:443
で起動して、それぞれをバーチャルホストとしてリクエストを割り振っている。
jwilder/nginx-proxyを使ってSSL化するには、jwilder/nginx-proxyでボリュームになっている
/etc/nginx/certs
にdomain-name.com.crt
とdomain-name.com.key
という形式のファイル名になっている証明書と鍵を保存したディレクトリをマウントすることで、
upstream domain-name.com {
## Can be connect with "bridge" network
# <container name>
server <container ip>:80;
}
server {
server_name domain-name.com;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
return 301 https://$host$request_uri;
}
server {
server_name domain-name.com;
listen 443 ssl http2 ;
access_log /var/log/nginx/access.log vhost;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;
ssl_prefer_server_ciphers on;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_certificate /etc/nginx/certs/domain-name.com.crt;
ssl_certificate_key /etc/nginx/certs/domain-name.com.key;
ssl_dhparam /etc/nginx/certs/domain-name.com.dhparam.pem;
add_header Strict-Transport-Security "max-age=31536000";
location / {
proxy_pass http://domain-name.com;
}
}
という様なconfファイルを内部で自動で生成してくれる。ただし、Let's Encryptでは
- 証明書:
/etc/letsencrypt/live/domain-name.com/fullchain.pem
- 鍵:
/etc/letsencrypt/live/domain-name.com/privkey.pem
という形式で証明書と鍵を保持しており、さらに、Dockerはシンボリックリンクのフォローをしてくれないので、これらのファイルをjwilder/nginx-proxyに識別させるためには
- シンボリックリンクを張って名前を変える
- 実体の入っている
/etc/letsencrypt/archive
を含めてマウントする
の両方を満たしてる必要がある。一番素直なやり方としては、まずホスト側のマシンで
# ln -s /etc/letsencrypt/live/domain-name.com/fullchain.pem /path/to/certs/domain-name.com.crt
# ln -s /etc/letsencrypt/live/domain-name.com/privkey.pem /path/to/certs/domain-name.com.key
という感じに証明書と鍵をまとめて
$ docker run -d -p 80:80 -p 443:443 \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
-v /etc/letsencrypt:/etc/letsencrypt:ro
-v /path/to/certs:/etc/letsencrypt/live:ro
jwilder/nginx-proxy
みたい起動してやれば良い。
アプリ側の設定について
上に書いたようにアプリにはjwilder/nginx-proxyから
proxy_pass
されてリクエストが到来することになるので、
nginx.confでの$schemeは'http'になっている。
これは結構重要で、というのは、jwilder/nginx-proxyの配下のコンテナのnginxでは
server {
### SNIP ###
location @ssr {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
という様に普通に更にアプリにproxy_pass
してしまうと、
X-Forwarded-Proto
は'http'
がセットされてしまいアプリ側でSSLが有効かどうかわからなくなってしまう。
なので、
server {
### SNIP ###
set $cutom_protocol $scheme;
if ($http_x_forwarded_proto = 'https') {
set $cutom_protocol 'https';
}
location @ssr {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $cutom_protocol;
さらに上位がSSLであるかチェックしてX-Forwarded-Proto
に上位のサーバーのプロトコルを渡してproxy_pass
してやると良い。
なんでこんなことを書くのか?
上に書いたようにendaaman.me
はSSRのために小さなExpressサーバーを持っているのだが、
アプリ側にSSLを強制したくないので、API fetchのために、http://endaaman.me
ならhttp://api.endaaman.me
へ、
https://endaaman.me
ならhttps://api.endaaman.me
というように切り替えたかった(ブラウザ側は単に//api.endaaman.me
で良い)。
そのためにはExpressにはhttpsでのアクセスなら、 上記のような設定を行った上で、さらに
const app = express()
app.enable('trust proxy')
としたサーバーにX-Forwarded-Proto: https
でリクエストが到達することで、
リクエストオブジェクトのreq.protocol
が'https'
になってくれるのである。
DHパラメータ
証明書と鍵だけの設定だとSSL Server TestでB評価止まりだが、
$ openssl dhparam 2048 -out /path/to/certs/domain-name.com.dhparam.pem
でDHパラメータを生成してマウントしたディレクトリにdomain-name.com.dhparam.pem
という名前でおいてやれば、
とくにSSL周りで特殊な設定は何もしなくてもA+評価まで取れる。
jwilder/nginx-proxyを使うことによる最大のメリット
それは、ドメインに対応する証明書と鍵の存在をチェックして動的にconfを生成してくれるので、鍵のない状態ならhttp
に、鍵を配置すれば全てhttps
にという感じでサーバーを一括で切り替えられるという点である。ローカル開発機では3つのenda.loca
、api.enda.local
、static.enda.local
を127.0.0.1
に紐づけることで3つ全てが完全な状態で動作するかの確認していて、https
での動作確認をするならオレオレ証明書SSLをマウントしてやれば全部https
になるし(もちろん警告は出る)、そうでないばあいであれば鍵をマウントしないだけで全てhttp
で動作してくれる。
これは個人的には結構なメリットだと思っている。
最後に
この辺の設定はendaaman/enda-serverで Ansibleのplaybookを作ってまとめて自動化してあるので、良かったら参考にしてください。