TT Lab
はじめる
学ぶ 学習パス コース

証明書は更新されたのに、ブラウザは古いものを表示した

更新ツールはファイルを変え、サーバはメモリを返す

TT Labで続きを見る

一言でいうと

更新ツールはファイルを書き換え、サーバーはメモリにある証明書を提示します。その間をつなぐ再起動(reload)がなければ、ディスクには新しい証明書があるのに、ブラウザーは古いものを見ます。そのため、更新の監視は、ファイルではなくサーバーが実際に提示する証明書を見る必要があります。

なぜ必要なのか

期限切れの3日前のアラートが来ました。更新作業はすでに昨日成功したと、ログに書かれています。ところがブラウザーは、相変わらず3日後に期限が切れる証明書を表示します。担当者はファイルを開いてみます。新しい証明書で間違いありません。この矛盾は、サーバーが証明書をいつ読み込むのかを知らなければ解けません。nginx・Envoy・JVMアプリ・Pythonのsslモジュールまで、ほとんどのサーバーは、証明書を起動時に一度だけメモリに載せます。ファイルが変わっても読み直しません。更新ツールがファイルを上書きしたことと、サーバーがそれを使うことは、別の出来事です。

このリポジトリでも、同じ種類の問いがありました。cert-managerが証明書を自動的に更新するよう設定を直しましたが、その設定では更新を一度も経験していない状態でした。「定期的に更新されるだろう」に答えるには、実際に一度実行させ、その後ブラウザーが受け取る証明書の日付が変わるかを見る必要がありました。判定の根拠は、Podの状態でもログでもなく、openssl s_clientが受け取ってきた証明書の日付でした。

どう動くのか

Pythonのsslモジュールで見ると、構造がそのまま現れます。SSLContext.load_cert_chain(certfile, keyfile)は、呼び出した時点でファイルを読み込んでコンテキストに入れ、その後のすべてのハンドシェイクは、コンテキストが持っているものを提示します。ファイルを上書きしても、コンテキストは気づきません。読み直させるには、プロセスを再起動するか、サーバーが提供する再読み込みのシグナル(nginxのreload、EnvoyのSDSのように、ファイルの変化を監視する仕組み)を使う必要があります。Kubernetesでcert-managerがSecretを更新すると、ボリュームとしてマウントされたファイルは少しあとに変わりますが、そのファイルを読むのは、依然としてアプリの役目です。再読み込みのロジックがないアプリは、Podを再起動する必要があります。

観察ツールはopenssl s_clientです。-connect host:portでハンドシェイクを行い、-servernameでSNIを送ったあと、受け取った証明書をx509に渡して、serial・有効期限・フィンガープリントを読みます。

$ echo | openssl s_client -connect 127.0.0.1:8443 -servername www.lab.internal 2>/dev/null \
    | openssl x509 -noout -serial -enddate
serial=2037629D1C3A75C63253D1BE593E399BA192FF71      ← 서버가 내보내는 것
$ openssl x509 -in /root/renew/live.crt -noout -serial
serial=2037629D1C3A75C63253D1BE593E399BA192FF72      ← 디스크에 놓인 것

2つのserialが違えば、事故です。監視スクリプトは、この2つの値を比べればよく、期限の監視は、ディスクのファイルではなく、s_clientで受け取った証明書に-checkendをかける必要があります。ファイルにかけると、「更新されたから安心」という偽の緑信号が点きます。

更新そのものは、同じ鍵で新しいCSRに署名する作業です。serialとvalidityだけが変わるので、鍵を替えなくても、証明書は新しいバージョンになります(鍵まで替えるほうが安全ですが、このモジュールの主題ではありません)。有効期間を短くしておくと、更新が頻繁に起きて、このような事故が早く表面化します。長くしておくと、年に1回、担当者が替わったあとで起きます。

現場での姿

症状はいつも、「更新は成功したというのに」で始まります。確認の順序は3つです。第一に、サーバーが提示する証明書をs_clientで受け取り、serialを見ます。第二に、ディスクのファイルと比べます。第三に、違えば再読み込みの方法を探します。どのプロセスかによって、reloadのシグナル、Podの再起動、サイドカーのSDS更新が答えになります。ロードバランサーの裏にサーバーが複数あると、1台だけが古い証明書を提示することもあるので、-connectに各サーバーのアドレスを直接入れて、1台ずつ見ます。更新の自動化を初めて有効にしたときは、期限切れを待たずに、一度強制的に更新を起こして、サーバーが新しい証明書を提示するところまで確認します。自動化は、実際の発行履歴があって初めて信頼できます。

次のラボですること

3日間のv1証明書でPythonのHTTPSサーバーを立ち上げ、s_clientでserialを読みます。同じ鍵で30日間のv2を発行してファイルを上書きしたあと、再起動していないサーバーが、依然としてv1を提示するのを観察します。ディスクとサーバーを比べるcertdiff.shと、サーバーが提示する証明書の有効期限を見るservedcheck.shを作り、採点ツールのサーバーで双方向の検査を受けます。再起動後にv2が提示されることを確認し、レポートに2つのserialを書きます。