jwilder/nginx-proxy で Qualys SSL Labs テストを 400 点満点にする

[Qualys SSL Labs テスト](https://www.ssllabs.com/ssltest/)を A+ にする記事はよく見ますが、400 点満点にする記事はあんまり見かけないので、個人用メモも兼ねて書いていきます。

![](https://qiita-image-store.s3.amazonaws.com/0/227889/2de43493-eddf-e850-02bd-471f6d4409ac.png)

# 試した環境

* [さくら VPS](https://vps.sakura.ad.jp) の最小プラン
* OS はデフォから [CentOS Atomic Host v7.4.1708](https://www.projectatomic.io) へと差し替え
    * [Docker Compose](https://docs.docker.com/compose/) をインストールしておく
* 独自ドメインが VPS と紐づいてる前提 (ここでは仮に _example.com_ とします)
* [Rapid-SSL](http://www.rapid-ssl.jp) ワイルドカード証明書を使う
    * [Docker](https://www.docker.com) + [jwilder/nginx-proxy](https://github.com/jwilder/nginx-proxy) を使って、_foo.example.com_ とかサブドメイン生やして、一台の VPS で複数のスタートアップなWebサービスを運用する想定

# 準備

1. DH key exchange を 4096bit で作成します。
   これが必要になるのは中盤になってからですが、CPU が弱いと 1KB も行かないファイルたった一つに __アホみたいな処理時間を要する__ ので、先にバックグラウンドで動かして、あとは放置して進めましょう。

    ```
    $ openssl dhparam -out dhparam.pem 4096
    ```

2. CSR 作成のために、__RSA 4096bit__ の秘密鍵を作ります。
   途中パスワードを求められますが、すぐ捨てるので雑なやつで構いません。

    ```SH
    $ openssl genrsa -aes256 -out example.com.p.key 4096
    
    Enter pass phrase for example.com.p.key: パスワードを作る
    Verifying - Enter pass phrase for example.com.p.key: パスワードを再入力

    $ openssl rsa -in example.com.p.key -out example.com.key

    Enter pass phrase for example.com.p.key: パスワードを再入力

    ここまででパスワードは忘れてよい
    $ rm example.com.p.key
    ```
   秘密鍵は __RSA 4096bit__ にします。2048bit でも問題なく作れますが、減点されます。
   ECDSA 好きとか 8Kbit 好きは、堅けりゃいいってもんじゃないことを肝に銘じておいてください！
   (Rapid-SSL で試したところ、いずれも怒られました)
   
3. 秘密鍵ができたら、次は提出用の CSR を作成します。

    ```SH
    $ openssl req -new -key example.com.key -out example.com.csr

    Country Name (2 letter code) []:JP <- 国記号
    State or Province Name (full name) []:Tokyo <- 都道府県名
    Locality Name (eg, city) []:Chiyoda-ku <- 市区郡名
    Organization Name (eg, company) []:Personal <- 法人名、個人なら Personal とか
    Organizational Unit Name (eg, section) []:SSL <- 部署名、なければ適当に入れる
    Common Name (eg, fully qualified host name) []:*.example.com <- 取りたい証明書のドメイン
    Email Address []:info@example.com <- メールアドレス
    A challenge password []: <- 何も入れずに Enter
    ```

4. Rapid-SSL へ CSR を提出し、メールアドレス認証と決済を済ませます。
   その後審査待ちとなりますが、大晦日のコミケ中に申請したところ 3 時間以内に発行されたので、すぐできると思います。

5. 発行された証明書と中間証明書を連結して、`example.com.crt` を作成します。
   連結はテキストエディタ使うなり、リダイレクトで追記するなりで OK です。

    ```PEM
    -----BEGIN CERTIFICATE-----
    MII...(SSLサーバ証明書 X.509)...=
    -----END CERTIFICATE-----
    -----BEGIN CERTIFICATE-----
    MII...(中間証明書 X.509)...=
    -----END CERTIFICATE-----
    ```
  
6. ルート証明書を用意します。
   [Rapid-SSL の場合、この辺からダウンロード](https://knowledge.Rapid-SSL.com/support/ssl-certificate-support/index?page=content&id=SO28616)して、ファイル名を `example.com.chain.crt` に変更します。
7. この時点で 5 つのファイルが揃いました。
   jwilder/nginx-proxy さんによしなにしてもらうために、今一度ファイル名と拡張子を確認しておきます。
   `dhparam.pem` がまだできていませんか？今から一睡でもすれば、きっと季節外れのサンタさんが用意してくれるでしょう。
    * `dhparam.pem`
    * `example.com.chain.crt`
    * `example.com.crt`
    * `example.com.csr`
    * `example.com.key`
8. サーバーにアップロードして、適当なフォルダに配置します。
   とりあえず今回は、例として下記のように置きました。
    * __`/opt/example.com/`__
        * __`certs/`__
            * `example.com.chain.crt`
            * `example.com.crt`
            * `example.com.csr`
            * `example.com.key`
        * __`dhparam/`__
            * `dhparam.pem`

# 実装

1. `docker-compose.yml` を作ります。

    ```YAML
    version: '2'
    services:

      proxy:
        image: jwilder/nginx-proxy:alpine
        container_name: example-proxy
        restart: always
        ports:
        - '80:80'
        - '443:443'
        volumes:
        - '/opt/example.com/certs:/etc/nginx/certs'
        - '/opt/example.com/dhparam:/etc/nginx/dhparam'
        - '/var/run/docker.sock:/tmp/docker.sock:ro'

      root:
        image: 'nginx:alpine'
        container_name: example-root
        restart: always
        environment:
          VIRTUAL_HOST: example.com

      foo:
        image: 'httpd:alpine'
        container_name: example-foo
        restart: always
        environment:
          VIRTUAL_HOST: foo.example.com
    ```
   各種証明書ファイルが jwilder/nginx-proxy コンテナと共有できていることがわかるかと思います。

2. 作ったファイルをサーバーに上げて、コンテナを立ち上げてみましょう。

    ```SH
    $ sudo docker-compose up
    ```

3. この時点でブラウザからアクセスして HTTPS で繋がることを確認できると思います。
   上手くいかない場合、`sudo docker-compose logs` すれば原因を追いかけやすいです。
4. では早速、[Qualys SSL Labs テスト](https://www.ssllabs.com/ssltest/)を試してみましょう。
   上手くいけば、375 点で __A+ 判定__ となるはずです。やったね！！

----

_おしまい_
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />

<small>_おしまい_ といったな、あれは嘘だ</small>

----

   そう、私はそれが自己満だと解っていても、400 点満点じゃないと納得できないんです！
   だから私はこの記事を書いたのです！！

# 400 点満点へ向けて改造する

1. 後に `docker-compose.yml` を編集することになりますが、編集中コンテナが生きてるとややこしいことになります。
   なので、まず立ち上げたコンテナをばくはしましょう。

    ```SH
    $ sudo docker-compose down
    ```

2. 400 点に向かうには `jwilder/nginx-proxy` イメージに細工を加える必要があります。
   下記のように `Dockerfile` ファイルを作ってください。

    ```Dockerfile
    FROM jwilder/nginx-proxy:alpine
    RUN \
        sed -i -e 's/max-age=31536000/max-age=31536000; includeSubDomains; preload/g' /app/nginx.tmpl && \
        sed -i -e 's/ssl_session_tickets off/ssl_session_tickets on/g' /app/nginx.tmpl && \
        sed -i -e "s/ssl_ciphers '\(.\+\)';/ssl_ciphers '\1:!aNULL!eNull:!EXPORT:!DES:!3DES:!MD5:!DSS:!AES128';/g" /app/nginx.tmpl && \
        sed -i '/ssl_dhparam/a\\ssl_ecdh_curve secp384r1;' nginx.tmpl && \
        sed -i '/add_header/a\        add_header Public-Key-Pins "pin-sha256=\\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\\"; pin-sha256=\\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\\"; max-age=5184000; includeSubDomains\\"";' nginx.tmpl
    ```
   次の手順に向かう前に軽く解説。
    * 1～2 行目については特に今更言うことはないですね。
    * 3 行目は HSTS preloading の設定。
    * 4 行目は Session resumption (tickets) の設定。
    * 5 行目は、弱い暗号化による通信を塞いでいます。
    * 6 行目は ECDH キー交換で使われる曲線名。デフォだと 256bit です。
    * 7 行目は HPKP の設定です。これは[もう要らん気もします](https://japan.cnet.com/article/35109624/)が、今回は設定してみます。

3. HPKP 用の公開鍵を作成します。

    ```SH
    $ openssl rsa -in example.com.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64
    ```
   `…=` で終わる 44 文字の羅列がベロっと吐き出されるので、それをコピーして、`Dockerfile` の 8 行目、__一番目の__ AAAA… に上書きでペーストします。
   二番目の AAAA… はそのまま放置してください。
   (本当は予備の秘密鍵こさえて、そこからちゃんと生成しなきゃダメだけど、スコアに影響しないし気にしなーい)

4. 先ほど作った `docker-compose.yml` を編集します。

    ```yaml
    version: '2'
    services:

      proxy:
        build: .
        # ↑ 差し替え (*1)
        container_name: example-proxy
        restart: always
        ports:
        - '80:80'
        - '443:443'
        volumes:
        - '/opt/example.com/certs:/etc/nginx/certs'
        - '/opt/example.com/dhparam:/etc/nginx/dhparam'
        - '/var/run/docker.sock:/tmp/docker.sock:ro'

      root:
        image: 'nginx:alpine'
        container_name: example-root
        restart: always
        environment:
          VIRTUAL_HOST: example.com
          SSL_POLICY: 'Mozilla-Modern'
          # ↑ 追加 (*2)

      foo:
        image: 'httpd:alpine'
        container_name: example-foo
        restart: always
        environment:
          VIRTUAL_HOST: foo.example.com
          SSL_POLICY: 'Mozilla-Modern'
          # ↑ 追加 (*2)
    ```
    * (*1) これは出来合いのイメージの代わりに `Dockerfile` でカスタマイズしたやつを適用するよって意味です。
    * (*2) これを入れると jwilder/nginx-proxy さんが空気読んで、古い通信規格や弱い暗号を弾いてくれます。

5. 2〜4 で作ったファイル群をサーバーに上げたら、コンテナを立ち上げます。
   今度は `Dockerfile` をコネコネするためにビルドが必要になります。

    ```SH
    $ sudo docker-compose build
    $ sudo docker-compose up
    ```

6. 改めてブラウザからアクセスして HTTPS で繋がることを確認できると思います。
   上手くいかない場合、`sudo docker-compose logs` すれば原因を追いかけやすいです。
7. 今一度、[Qualys SSL Labs テスト](https://www.ssllabs.com/ssltest/)を試してみましょう。
   上手くいけば、__400 点で A+ 判定__ となるはずです。今度こそやったね！！
8. [HSTS Preload](https://hstspreload.org) に登録しとくと、HTTP で アクセスしてきた人をブラウザ側で HTTPS にリダイレクトしてくれるから、もっとセキュアになるよ！

# スコアを上げるデメリットとメリット

## デメリット

古いブラウザ環境で死にます。

とは言っても、死ぬのは _IE9 未満とか iOS6 未満とか Android 4.1 未満とか、_ そういう本当にレガシーな環境ばかりなので、スコアを抑えてまで救済するメリットはほとんどないんじゃないかなと。

(2018.1.8 追記)
満点にすると、__[Twitter のカード表示](https://cards-dev.twitter.com/validator)で死にます。__
暗号化スイートとして、`ECDHE-RSA-AES128-GCM-SHA256` が有効である必要がありますが、この暗号化は AES128 という 256bit 未満の暗号化が含まれており、これがあると 390 点で頭打ちになります。ただ、A+ 判定であることに変わりはない上、[Mozilla wiki でも執筆時点では Modern と見做されている暗号化スイート](https://wiki.mozilla.org/Security/Server_Side_TLS)なので、これによるセキュリティの低下を気にするのは杞憂と言えるでしょう。

## メリット

少なくとも、低いスコアよりはセキュアになります。

一般的には C〜B くらいは最低限上げておくべきと思います。
D ランク以下の場合は、何らかの重大な脆弱性を抱えている場合が多いです。

ただし A+、ましてや 400 点満点にするのは、ちょっと自己満足の世界に近いかもしれません。
実際、私は~~[実際にこの方法で 400 点満点となった Web ページ](https://www.ssllabs.com/ssltest/analyze.html?d=micomi.co)を持っています~~(※デメリットの項目参照)が、これのおかげでモテるようになったかというと、そういう経験は今の所ありません。

また当たり前の話ですが念のため。
__このスコアを上げても、セキュアになるのは HTTPS(TLS) 通信だけです。__ SSH とかそういう別のところに穴があるとかいう話は往々にしてあることなので、このスコアにこだわり過ぎず、また満点だからって慢心しないことが大事です。
(過去の経験だとやたら常時 SSL を推す割に、デプロイが生の FTP だったりとかあった)