プライベートCAで証明書を発行しnginxに適用する
目標
プライベートCAを作って、SANを含むサーバー証明書を発行し、チェーンファイルを構成してnginxにHTTPSを適用し、期限監視スクリプトまで作れるようになります。
なぜ重要なのか
証明書障害の大半は、暗号学ではなく、チェーンの欠落と期限切れの2つです。中間証明書を抜かすと、ブラウザーでは動くのに、連携相手のシステムでだけ失敗します。ブラウザーは中間証明書をキャッシュしたり、自動的に取得したりしますが、サーバー間のHTTPクライアントは、そうしないからです。「開発者のPCでは動くのですが」は、この症状の典型です。そして、期限切れは予告なしには来ないのに、担当者の交代とアラートの漏れで、毎年事故が起きます。プライベートCAで一度自分で作ってみれば、この構造が身に付きます。
ステップ
/root/ng/tlsディレクトリを作成して、RSA 2048ビットの秘密鍵を/root/ng/tls/server.keyとして生成します。- その鍵でCSRを作成して、
/root/ng/tls/server.csrに保存します。サブジェクトにCN=labhub.local、O=LabHub、C=KRを含める必要があります。 - プライベートCAを作ります。
/root/ng/tls/ca.keyと/root/ng/tls/ca.crtです。CA証明書のサブジェクトはCN=LabHub Root CAで、有効期間は3650日以上、basicConstraintsにCA:TRUEがある必要があります。 - CAでCSRに署名して、
/root/ng/tls/server.crtを作成します。SANにDNS:labhub.localとIP:127.0.0.1の両方が入っている必要があり、発行者(issuer)がCAのサブジェクトと同じである必要があります。 /root/ng/tls/fullchain.pemを作成します。サーバー証明書 → CA証明書の順につなげ、ファイルの中に証明書がちょうど2つある必要があります。openssl verify -CAfile ca.crt server.crtが成功する必要があります。- nginxにポート8443のHTTPSサーバーブロックを追加します。
ssl_certificateはfullchain、ssl_certificate_keyはserver.key、ssl_protocolsはTLSv1.2 TLSv1.3だけを許可します。openssl s_client -connect 127.0.0.1:8443 -CAfile /root/ng/tls/ca.crtの結果に、Verify return code: 0が出る必要があります。 - ポート8088のすべてのリクエストが、
https://へ301リダイレクトされるようにします。curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1:8088/xの結果が301で、リダイレクトURLがhttps://で始まり、8443を含む必要があります。 /root/ng/certcheck.shを作成します。引数を2つ(인증서파일 임계일수、韓国語の語は、順に「証明書ファイル」「しきい値の日数」を意味します)受け取って、残りの有効期間がしきい値の日数より多ければ終了コード0、しきい値の日数以内に迫っていれば、0以外の終了コードで終わります。証明書ファイルがなければ、やはり0以外の終了コードで終わる必要があります。
参考
- 鍵の生成:
openssl genrsa -out server.key 2048 - CSR:
openssl req -new -key server.key -out server.csr -subj "/C=KR/O=LabHub/CN=labhub.local" - 自己署名CA:
openssl req -x509 -new -key ca.key -days 3650 -out ca.crt -subj "..."(CAの拡張は-addext "basicConstraints=critical,CA:TRUE"で指定できます) - 署名時のSANの注入: 拡張ファイルを作って、
openssl x509 -req ... -extfile <파일>(プレースホルダーはファイルです) - 期限の確認:
openssl x509 -in <파일> -noout -checkend <초>(プレースホルダーは、順にファイルと秒数です) - よくあるミス1: SANなしでCNだけを入れて、「なぜブラウザーが拒否するのか」を繰り返すミスです。
- よくあるミス2: fullchainの順序を、CAが先になるように作るミスです。サーバー証明書が先です。
- よくあるミス3:
-checkendの引数を「日」で指定するミスです。単位は秒です。
サーバー秘密鍵の生成
/root/ng/tlsディレクトリを作成して、RSA 2048ビットの秘密鍵を/root/ng/tls/server.keyとして生成します。
RSA 2048ビットが、今も標準的な選択です。生成した鍵が正常かを確認するopensslのサブコマンドも、一緒に身に付けておいてください。
CSRの生成
その鍵でCSRを作成して、/root/ng/tls/server.csrに保存します。サブジェクトにCN=labhub.local、O=LabHub、C=KRを含める必要があります。
CSRのサブジェクト情報は、-subjで一度に指定できます。国コードは、2文字の大文字です。
プライベートCAの作成
プライベートCAを作ります。/root/ng/tls/ca.keyと/root/ng/tls/ca.crtです。CA証明書のサブジェクトはCN=LabHub Root CAで、有効期間は3650日以上、basicConstraintsにCA:TRUEがある必要があります。
CA証明書は、通常のサーバー証明書と違う必要があります。どの拡張がCAであることを示すかを、確認してください。openssl reqに-x509を指定すると、自己署名証明書ができます。
CAによるサーバー証明書への署名
CAでCSRに署名して、/root/ng/tls/server.crtを作成します。SANにDNS:labhub.localとIP:127.0.0.1の両方が入っている必要があり、発行者(issuer)がCAのサブジェクトと同じである必要があります。
最近のブラウザーとライブラリは、CNではなくSANを見ます。openssl x509で署名するとき、拡張をファイルで渡さないと、SANが入りません。
チェーンファイルの構成
/root/ng/tls/fullchain.pemを作成します。サーバー証明書 → CA証明書の順につなげ、ファイルの中に証明書がちょうど2つある必要があります。openssl verify -CAfile ca.crt server.crtが成功する必要があります。
サーバー証明書とCA証明書をつなげます。順序が重要です。作ったあとは、検証コマンドで、実際に信頼のパスが成立するかを確認してください。
nginxへのHTTPS適用
nginxにポート8443のHTTPSサーバーブロックを追加します。ssl_certificateはfullchain、ssl_certificate_keyはserver.key、ssl_protocolsはTLSv1.2 TLSv1.3だけを許可します。openssl s_client -connect 127.0.0.1:8443 -CAfile /root/ng/tls/ca.crtの結果に、Verify return code: 0が出る必要があります。
証明書と鍵がペアでないと、nginxは起動すらしません。プロトコルは、非推奨のバージョンを除いて指定してください。
HTTP → HTTPSのリダイレクト
ポート8088のすべてのリクエストが、https://へ301リダイレクトされるようにします。curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1:8088/xの結果が301で、リダイレクトURLがhttps://で始まり、8443を含む必要があります。
rewriteよりreturnのほうが、明確で速いです。ステータスコードとLocationヘッダーの両方が合っている必要があります。
期限の監視スクリプト
/root/ng/certcheck.shを作成します。引数を2つ(인증서파일 임계일수、韓国語の語は、順に「証明書ファイル」「しきい値の日数」を意味します)受け取って、残りの有効期間がしきい値の日数より多ければ終了コード0、しきい値の日数以内に迫っていれば、0以外の終了コードで終わります。証明書ファイルがなければ、やはり0以外の終了コードで終わる必要があります。
openssl x509には、「残りの有効期間がN秒以上か」を終了コードで教えてくれるオプションがあります。監視スクリプトはこれを使えば、日付のパースが不要です。