4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-02

おことわり

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

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

モダンに行こうぜ

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

雛形

MozillaさんがSSL Configuration Generatorという素晴らしいものを公開してくださっています。
こちらでサーバーセキュリティレベル環境オプションを選択すると自動で最適な設定を教えてくれます。
セキュリティレベルには以下の三段階があります。

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

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

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

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

注意

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

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

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

環境

$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などを記していきます。
まずは参考に僕の最終的な設定を貼っておきます。

/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

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

SSLの種類 & 暗号の種類

いきなり核心、そして革新です。

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

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

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

Session resumption

思いっきり躓いたところです。
どのサイトを見ても「**ssl_session_cache**書けばええで!」と書いてありますが、SSL labsではNo (IDs assigned but not accepted)と言われます。
こちらのブログにあるとおり、正しい位置はserver {}の外で、http{}に直書きすればよいそうです。
http{}の中というのは一瞬戸惑いますが、/etc/nginx/sites-enabled/default/etc/nginx/nginx.conf内のhttp{}内にincludeされますので、直に書けばよいということになります。

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

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

HSTS & X-Frame-Options

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

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

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

Forward Secrecy & DHE Parameters

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

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

前述のサイトでは暗号化キーを出力するパスが間違っていて(/etc/ssl/certsdhparam.pem)エラーになるので注意ください。

OCSP Stapling

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

    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のオプションを削除します。

$ 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 444nginxの独自コードで、低負荷でアクセス拒否できます。

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

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

HTTP/2

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

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

Google chromeのデベロッパツールのNetworkタブでテーブルのヘッダーを右クリックすると表示項目を選べますので、Protocolにチェックをつければ通信を確認できます。
h2と表示されればHTTP/2で通信ができています。
image.png
ちなみにビルドやらなんやらをすればHTTP/3もいけるのだとか。
手が届くところに来たら実装してみたいと思います。

SSL labs

ここまで設定すればA+だろうと思うんですが、TLSv1.3だけにするとTLSv1.2Noとなってしまいますので評価はAです。ええぇぇぇ(激寒)
image.png
緑色のThis server supports TLS 1.3.が気持ちいいですね。
ピンクとかでThis server supports ONLY TLS 1.3.とか書いてくれてもいいんですが…。

そしてご覧ください、この対応環境の少なさ。快感です。
image.png

追記

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

まとめ

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

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

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?