nginxを自己満足でセキュリティ強化する（TLSv1.3）

# おことわり
>以下を読んで「は？」って思う方々が読み進むことは推奨できません。
>`俺は下位互換性なんて知らねぇ！
>最新に対応していないユーザーが悪いんだ！`

ゆっくりしていってね！！！

# モダンに行こうぜ
僕の趣味は最新に頑張ってついていくことです。
ですので自分しか見ないWebサーバーですが最新の`TLSv1.3`**だけ**に対応させて悦に浸ろうと思います。
しかし、致命的なことに猿と同じ哺乳類なのでビルドとか難しいことはできません。
`apt`で手に入るバージョンだけを使って簡単に無駄にハイセキュアな自己満環境を作っていきたいと思います。
幸い現行バージョンではそれが可能です。

# 雛形
Mozillaさんが[SSL Configuration Generator](https://ssl-config.mozilla.org/)という素晴らしいものを公開してくださっています。
こちらで`サーバー`と`セキュリティレベル`、`環境`、`オプション`を選択すると自動で最適な設定を教えてくれます。
セキュリティレベルには以下の三段階があります。

>**Modern**
スパルタ系セキュリティ
下位互換性なんて知らねぇよ！最新の俺についてこい！

>**Intermediate** (言うまでもないが常識的にはここがオススメ)
やさしさ系セキュリティ
貴方のような古い機種を使っている人も守ってあげますわ。

>**Old**
優しすぎてむしろスパルタ系セキュリティ
うちはー…自治を尊重してるんで…、自分の身は自分で守ってくださーい。

さて、あなたの落としたセキュリティ設定はどれですか。
当然`Android 10`をお使いの皆様は、当然`Modern`を選択しますね？

## 注意
`Modern`を選ぶのは当然趣味なので構わないのですが、きちんと設定をしないとユーザーが`http`で通信することになったりしてむしろ危険です。
**`TLSv1.3`か`Deny`か**の二択を突き付けられるような環境でのみ実装してください。
例え`old`を選択したとしても、基本的に`http <<<<< old`です。
`http`での通信は猿がｳｯｷｬｧｧｧって叫んでいるのと同様に丸見えで丸聞こえで原始的です。

それと上のセキュリティレベルの説明は誇張してあります。
例え`old`でもきちんと（きちんと！）設定すれば互換性を考えた素晴らしいサイトになりますので、あくまでもこれは雛形ということです。

ていうか`old`レベルでもいいから全企業サイトはHTTPSに対応してくれ…。

## 環境

```bash
$uname -a
Linux raspberrypi 5.4.51-v8+ #1333 SMP PREEMPT Mon Aug 10 16:58:35 BST 2020 aarch64 GNU/Linux
$nginx -v
nginx version: nginx/1.14.2
```

## 前提条件

- `certbot`を使い`Let's Encrypt`で証明書取得済み
- ネットワークがipv6対応済み
- 読者の皆様は、猿と同じ哺乳類の僕よりは頭が良い

# 設定
前置きが長くなってしまいました。
要するに、今回のお題は「`modern`を打ち出して設定すればめっちゃセキュアじゃん」ってことなんですが、当然コピペじゃ動かないのでハマったことやTipsなどを記していきます。
まずは参考に僕の最終的な設定を貼っておきます。

```bash:/etc/nginx/sites-enabled/default
# generated 2020-10-02, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=modern&openssl=1.1.1d&guideline=5.6

# Deny IP-direct access
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        listen 443 ssl http2 default_server;
        listen [::]:443 ssl http2 default_server;

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        ssl_protocols TLSv1.3;
        ssl_prefer_server_ciphers off;

        server_name _;
        return 444;
}

# Standby for example.com
server {
        root /var/www/example.com;

        index index.php index.html;
        server_name example.com;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        }

        location ~ /\.ht {
                deny all;
        }

        # SSL configuration
        listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
        listen 443 ssl http2; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot

        ssl_protocols TLSv1.3;
        ssl_prefer_server_ciphers off;

        ssl_dhparam /etc/ssl/certs/dhparam.pem;
        ssl_ecdh_curve secp384r1;

        ssl_session_timeout 10m;
        ssl_session_tickets off;

        add_header Strict-Transport-Security "max-age=63072000" always;
        add_header X-Frame-Options "DENY";

        # OCSP stapling
        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8 8.8.4.4 valid=300s;
        resolver_timeout 5s;
        ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
}

# Redirect http to https
server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;
    server_name example.com;
    return 404; # managed by Certbot
}

ssl_session_cache shared:SSL:10m;

```

ご覧の通り基本的に`certbot`による設定をそのまま使ってます。
最近はだいぶ優秀ですが、上手くいくコツは設定をいじっていない状態で走らせることです。

設定のポイントは以下で。

# Strong SSL Security on nginx

前述の雛形を参考にしながら、[こちらの内容](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html)をそのまんま実行していきます。
とても分かり易くまとまっていて、かつ色んなサーバーに統一的に対応しているのが素晴らしい解説です。
全て漏らさず正確に記す自信はないので、各項目の細かい解説は上記リンクを参考にするかご自身で調べてください。~~（丸投げ）~~
一応見出しをリンクにしてあります。

## [SSLの種類 & 暗号の種類](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#toc_0)
いきなり核心、そして革新です。

```bash
    ssl_protocols TLSv1.3; # Only TLSv1.3!!
   #ssl_ciphers TLSv1.3では明示しません
    ssl_prefer_server_ciphers off; # TLSv1.3ではoffにします
```

`TLSv1.3`は従来のプロトコルと違いゴリゴリっと刷新されたものですから、設定もだいぶ変わってきます。
暗号スイートは明示せずに、`ssl_prefer_server_ciphers`も`off`に設定します。
違和感しかありませんがこれで良いそうです。

ちなみに、Certbotで普通に設定すれば`TLSv1.2`と`TLSv1.3`の両方が有効になるはずです。
それでも[SSL Labs](https://www.ssllabs.com/ssltest/analyze.html)で`TLSv1.3`が有効になっていない人は、メインユース以外のServerブロック（ip直打ち拒否用など）にSSLの設定がないかを確認してください。
なぜかすべての設定のなかで一番低いものに引っ張られるようですので、すべての設定に`TLSv1.3`を書き加えましょう。

## [Session resumption](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#toc_4)

思いっきり躓いたところです。
どのサイトを見ても「**`ssl_session_cache`**書けばええで！」と書いてありますが、[SSL labs](https://www.ssllabs.com/ssltest/analyze.html)では`No (IDs assigned but not accepted)`と言われます。
[こちらのブログ](https://kitani3.blogspot.com/2016/10/ssl-sever-test-session-resumption.html)にあるとおり、正しい位置は`server {}`の外で、`http{}`に直書きすればよいそうです。
`http{}`の中というのは一瞬戸惑いますが、`/etc/nginx/sites-enabled/default`は`/etc/nginx/nginx.conf`内の`http{}`内に`include`されますので、直に書けばよいということになります。

```bash
server {
    ssl_session_timeout 10m;
    ssl_session_tickets off;
}
ssl_session_cache shared:SSL:10m;
```

こちらを設定しておくと、サーバーの負荷が軽減されます。
雛形ではキャッシュの期間が`1d`（一日）になっていますが、この長さはセキュリティの高さに反比例しますので`5m-10m`ぐらいが適当だろうということです。

## [HSTS & X-Frame-Options](https://raymii.org/s/tutorials/HTTP_Strict_Transport_Security_for_Apache_NGINX_and_Lighttpd.html)

どちらもセキュリティを高める設定です。
こちらの設定は一度アクセスされると`max-age`秒の間クライアント側で保持され続けます。
要するに、いきなり以下のように設定して証明書なんかが間違っていたら2年間アクセスできなくなりますので、最初は`max-age=300`なんかにして正しく設定されていることを確認してから延ばすとよいと思います。

```bash
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Frame-Options "DENY";
```

以前は`includeSubdomains`なんてごちゃごちゃ書いてましたがすっきりしました。

## [Forward Secrecy & DHE Parameters](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#toc_5)

こちらもセキュリティを高める設定です。
設定の前に以下のコマンドで暗号化キーを生成します。
なぜかシングルコアしか使えず時間がかかるのでご注意ください。（Raspi 4で5時間）
`openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096`

```bash
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_ecdh_curve secp384r1;
```

前述の[サイト](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#toc_5)では暗号化キーを出力するパスが間違っていて（`/etc/ssl/certsdhparam.pem`）エラーになるので注意ください。

## [OCSP Stapling](https://raymii.org/s/tutorials/OCSP_Stapling_on_nginx.html)

ここも何気につまずいたところです。
証明書と同ディレクトリにある`README`を読むと分かるのですが、`certbot`で認証を行うと自動で**OCSP Stapling**で使う`ssl_trusted_certificate`用の`root_CA_cert_plus_intermediates`（ルート証明書+中間証明書）を作成してくれます。
二つのファイルを繋げて云々と試行錯誤していたのですが一発で解決しました。

```bash
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
```

また、設定できたかどうかを確認するコマンドとして`openssl s_client -connect example.com:443 -tls1 -tlsextdebug -status`をよく見かけますが、本設定では`TLSv1.3`のみの対応なので、このままでは`OCSP response`の項目すら出てきませんのから`-tls1`のオプションを削除します。

```bash
$ openssl s_client -connect example.com:443 -tlsextdebug -status

(...)
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: Oct  2 03:09:00 2020 GMT
    Responses:
    Certificate ID:
      Hash Algorithm: sha1
      Issuer Name Hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX #別に隠匿する必要もないんですが…。
      Issuer Key Hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
      Serial Number: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    Cert Status: good # こうやって出てくればOK この項目名でgrepするのもアリ
(...)
```

一応、テストの前にサイトにアクセスするようにしてください。
割と数時間とかで登録が消えます。

# 徒然なるままに

以下は僕の設定について余談です。
セキュリティに関するところだけ拾い上げているつもりです。

### IP直打ち拒否

ログをチェックするとめちゃくちゃ不正アクセスがあります。
大抵IP直打ちでアクセスしてきますので拒否しましょう。
`nginx`はアクセスしてきたドメイン名毎に仮想のサーバーを提供できます。
それを利用して、`運用サーバー（example.com）`に引っかからなかった`それ以外（IP直打ち）`のアクセスを受けるサーバーを立てて`default_server`とし、アクセスに対し`444`を返すようにします。
`status 444`は`nginx`の独自コードで、低負荷でアクセス拒否できます。

```bash
server {
        listen 80 default_server; # このdefault_serverを複数のドメインにつけるとエラーになるので
        listen [::]:80 default_server; # IP拒否のこのserverブロックにだけ付与する

        listen 443 ssl http2 default_server; # SSLでも待ち受けてあげる
        listen [::]:443 ssl http2 default_server; # IPv6でも

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        ssl_protocols TLSv1.3; # ここにも指定しないとTLSv1.3が反映されないという落とし穴
        ssl_prefer_server_ciphers off;

        server_name _; # どこにもひっかからなかった、という意味
        return 444; # さようならー
}
```

### リダイレクト
リダイレクトは色々な設定がありますが`certbot`のデフォルトもそれなりにスマートなのでそのまま採用です。
ポート80への`example.com`のアクセスを`if`で場合分けし、`301`とともにリダイレクトさせ、それ以外の場合は`404`を返します。

```
server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;
    server_name example.com;
    return 404; # managed by Certbot
}
```

## [HTTP Public Key Pinning Extension](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#toc_8)

こちらの機能は廃止された機能です。
過去の安全を支えてくれ、今日のセキュリティの礎となった技術に敬意を払いながら、実装しないように無視しましょう。

## HTTP/2

本設定ではHTTP/1.1の後継であるHTTP/2にも対応しています。
もう大方普及してきたかと思いますが、体感できる程の違いがあるそうなので実装しておきたいですね。
設定は簡単、`listen`に`http2`を追記するだけです。

```
    listen [::]:443 ssl http2 ipv6only=on;
    listen 443 ssl http2;
```

Google chromeのデベロッパツールの`Network`タブでテーブルのヘッダーを右クリックすると表示項目を選べますので、`Protocol`にチェックをつければ通信を確認できます。
`h2`と表示されれば`HTTP/2`で通信ができています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684034/becab71e-b0d9-a01e-5a84-423a44bc8a50.png)
ちなみにビルドやらなんやらをすればHTTP/3もいけるのだとか。
手が届くところに来たら実装してみたいと思います。


## [SSL labs](https://www.ssllabs.com/ssltest/analyze.html)

ここまで設定すれば`A+`だろうと思うんですが、`TLSv1.3`だけにすると`TLSv1.2`が`No`となってしまいますので評価は`A`です。ええぇぇぇ（激寒）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684034/a99c07d0-d8bf-63d6-99c0-9568ffa0461f.png)
緑色の`This server supports TLS 1.3.`が気持ちいいですね。
ピンクとかで`This server supports ONLY TLS 1.3.`とか書いてくれてもいいんですが…。

そしてご覧ください、この対応環境の少なさ。快感です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684034/a9733317-138b-d22a-ede8-399291aa9897.png)

# 追記

このサーバーではHLSでライブストリーミングを実装しているのですが、（ブラウザでの視聴は問題ないものの）`MX Player`がTLSv1.3に対応していませんでした。
MX PlayerでTLSv1.3のみのストリーミングURL（`.m3u8`）にアクセスすると`このURLは再生できません`と言われてしまいエラーになります。
このサーバーの目的は主にストリーミング視聴なので、ロマンか利便性か…、迷いますね。

# まとめ
まず、記述漏れがあったらすみません。
なにか脆弱性や問題点、僕の頭の脆弱性などを見つけられた方はお教えください。

再度言いますが、リダイレクトと拒否の設定をきちんと行わないとかえって安全ではなくなります。
[SSL Labs](https://www.ssllabs.com/ssltest/analyze.html)の評価を見ても分かる通り、正直言って実用性はあまりない設定です。
それでも自分しか使わないのだからロマン重視で行きたいという記事でした。

僅かでもお役に立てれば幸いです。