こんにちは id:cohalz です。はてなブログでは2021年4月の公式ブログで、すべてのブログをHTTPSに一本化していくことを案内しました。
▶ 「HTTPS配信」への切り替えと、ブログの表示の確認をお願いいたします
この時点でまだ数百万件のHTTPのブログが残っている状態でしたが、2021年8月には上記の案内に追記したように、全ブログでHTTPS化を完了できました。
完了までに行ってきたことをこの記事で振り返ってみようと思います。
はてなブログのHTTPS化のこれまで
はてなブログのHTTPS化は、2017年9月に最初のお知らせを行ってスタートしました。
当初の予定より時間がかかりましたが、2018年2月にHTTPS配信の提供を開始し、これ以降に作成されたブログは最初からHTTPSのみで配信されています。また、それ以前に作成されたブログでも、ユーザ側で設定を変更することで自分のブログをHTTPS化できるようにしていきました。
はてなが提供するドメインについては、2018年4月に対応を完了しました。2018年6月には独自ドメインのHTTPS配信が完了し、ここで開発も一段落となりました。
2018年までに取り組んだHTTPS化の詳細に関しては、当時の担当エンジニアのブログにまとまっています。
なぜこれまでHTTPS化を強制しなかったか
2018年までの取り組みでは、以前から存在するブログすべてにHTTPS化を強制したわけではなく、ユーザが自分で変更するオプトインの形式にしていました。
HTTPS化を強制しなかったのは、ユーザ側でHTTPの画像やスクリプトを配置している場合に、Mixed Contentが発生して表示が崩れてしまうおそれがあるためです。ユーザが必ず自分で表示を確認し、同意を得るという意味がありました。
Webブラウザの更新にともなってHTTPのブログに警告が表示されるなども起きていますが、問題があればユーザが個別にHTTPS化のボタンを押せば対応できました。このため、強制的に全ブログをHTTPS化する優先度はなかなか上がらない状況でした。
強制的にHTTPS化していくことにした経緯
そうした中、Webブラウザにおけるセキュリティやプライバシーの改善が進むにつれて、HTTPのブログではいくつかの機能が使えなくなり、ユーザからの問い合わせの頻度もどんどん増えていました。具体的には、ブログ上部の共通ヘッダーでログイン状態が取得できない、あるいははてなフォトライフに画像をアップロードできないなどの不具合が発生しました。
こうした不具合には対応できるものも対応できないものもあり、対応できる場合にもそのたび場合分けが必要となり、コードも複雑になってしまう状況でした。そういった対応は今後も増えていくことが予想されるので、さすがこれ以上は後回しにできないと判断し、強制的にHTTPS化していくことを決めました。
全ブログをHTTPS化するために必要だったこと
残った数百万のブログをHTTPS化するまでに、次のような作業を実施しました。
- 独自ドメインのHTTPS配信に関するモニタリングを強化する
- はてなドメインの証明書もLet's Encryptに統一して処理を自動化する
- Mixed Content解消のためupgrade-insecure-requestsを導入する
- TLS接続のパフォーマンスを改善する
- 独自ドメインの検証対象にCAAレコードを含める
- HTTPS移行の速度を上げるためにキャッシュ削除で工夫する
- 移行するブログ数を把握するためBigQueryを活用する
この記事で順に説明していきたいと思います。
独自ドメインのHTTPS配信のモニタリング強化
HTTPSの接続を前提とするには、証明書が正しく発行され、正しく配信されていることに、より関心を向けていく必要があります。
はてなブログでは有料オプションの機能として、ユーザーが自分で取得したドメインを使用できるようにしています。そのため、独自ドメインのブログがたくさんあり、それぞれの証明書を適切に管理する必要があります。
証明書を正しく管理できているかを把握する必要性
これまでも証明書の発行に関しては、エラー率などある程度のモニタリングはできていました。しかし、発行に失敗したあと有効期限が切れるまで時間差があるため、発行エラーが後々の配信に影響を与えるかどうかまでは、そのタイミングで判断が難しい状態でした。
実際、利用者から「証明書の有効期限が切れた」という問い合わせをもらって、手動で再発行の対応をすることも何度かありました。
こうした状況で、証明書を正しく発行でき配信できているかを把握するため、下のシステム構成図の配信側(cert-cache-gw)でもモニタリングを強化することにしました。
ポイントは以下の2つです。
- リクエストのうち有効期限が切れてない証明書のどの程度の割合で返せているか
- 期限を迎えた証明書を返してしまったドメインをすぐ把握するできるようにする
有効期限が切れてない証明書を返せた割合を確認
まず、Goのexpvarパッケージを使って、有効期限が切れた証明書を返した数などを内部でカウントするようにしました。
$ curl -s 127.0.0.1:8888/api/metrics/certs | jq . { "cache.hits": 4, "cache.misses": 1, "cert.already_expired": 1, "cert.soon_to_be_expired": 0, "cert.valid": 4, "requests": 7, "status.bad_request": 0, "status.internal_server_error": 0, "status.not_found": 2, "status.ok": 5 }
そこで取得したJSONを、mackerel-plugin-jsonを使ってMackerelにメトリックとして投稿します。mackerel-plugin-jsonでは-diff
オプションでカウンター値の1分間の差分を取ることができ、1分ごとのリクエスト数などをそのまま投稿できます。
これを元に、リクエストのうち有効期限が切れてしまった証明書の割合などを、よりリアルタイムにMackerel上でモニタリングできるようになりました。
mackerel-plugin-jsonに関しては次の記事も参照してください。
有効期限を迎えた証明書を見つける
さらに証明書の有効期限が近かったり、有効期限を迎えてしまったブログをログに出力し、集計できるようにもしました。
これにより問い合わせがなくても、問題が起きたブログをすぐ発見できるようになりました。
証明書をLet's Encryptに統一して自動化
Let's Encryptは非営利団体のISRGが運営する認証局で、すべてのWebサイトに安全な接続を提供するため、2014年から証明書を無料で発行しています。
はてなブログでは、もともとLet's Encryptの利用は独自ドメインのブログのみで、はてなで提供している*.hatenablog.com
や*.hatenadiary.jp
といったドメインは商用の認証局が発行する有償の証明書を利用していました。
これを止めて、Let's Encryptに統一しました。これは主に以下の2つの理由によります。
- サポートする範囲の統一
- 更新作業の自動化
サポートする範囲を統一する
証明書の発行元が異なると、*.hatenablog.com
にはつながるが独自ドメインのブログにはつながらない(またはその逆)といった事態が起こりえます。そういった問題をできる限りなくしたい気持ちがありました。
独自ドメインのHTTPS配信にLet's Encryptを使用しないという選択肢が現実的でない以上、独自ドメイン側に合わせる形ですべてのドメインがLet's Encryptを利用するようにしました。
証明書の更新作業を自動化する
有償の証明書は購買から入れ替えまでのフローに多くの手順が必要で、自動化も難しい状態でした。その点、Let's Encryptの証明書は自動更新を前提としており、次の記事で紹介しているようにはてなブログで利用する仕組みも社内に存在していたため、これに乗り替えました。
また、2018年3月以降には2年を超える証明書が発行できなくなり、2020年9月以降には1年を超える証明書が発行できなくなるなど、ここ数年で証明書の最長の有効期限がどんどん短くなっています。これによって更新が必要な頻度も増えたため、自動化のモチベーションが高まったこともあります。
2018年の初めに購入した3年間有効な証明書を使い続けていましたが、これが切れる前にすべてLet's Encryptに入れ替え終わり、更新作業の必要もなくなりました。
CSP標準を利用してMixed Contentを解消
それまでHTTPだったブログにHTTPSで接続するようになると、読み込まれている画像やスクリプトによってMixed Contentが発生してしまう可能性があります。
はてなで提供する画像などの貼り付けは既にHTTPS対応していますが、ユーザが自由にスクリプトなどを貼り付けられる仕様上、強制的にHTTPS化するとブログの表示が急に崩れてしまうことが数多く発生してしまうと予想されました。
upgrade-insecure-requestsを指定したい
これを回避するため、HTTPのCSP(Content Security Policy)標準には、upgrade-insecure-requests
という仕組みが存在します。
これを有効にするとMixed Contentは解消できますが、代わりにHTTPでしか配信されていないリソース(画像なども含む)が読み込めなくなるという大きなデメリットもあります。2018年当時はまだHTTPSで配信されてないサイトも存在しており、はてなブログ一括で有効にすることは諦め、案内を出すに留めたという経緯もありました(スクリーンショットは当時のレビュー)。
しかし、多くのMixed Contentが解消されるメリットはかなり大きいため、3年が経過した2021年に再び検討することになりました。
Content-Security-Policy-Report-Onlyによる検証
検討のため、Content-Security-Policy
ヘッダでupgrade-insecure-requests
を指定する代わりにContent-Security-Policy-Report-Only
ヘッダを利用し、Mixed Contentが発生したブログや読み込むリソースのURLを送信して、そのログを可視化してみました。
読み込みがブロックされたリソースに関して、ドメインごとにグルーピングし(上の図の左側)、それがHTTPSでも配信されているかを1つ1つ見ていくことで、HTTPSへの対応状況を確認しました。
その結果、HTTPSで配信できないリソースはほぼ存在しておらず、新規に読み込めなくなってしまう状況は少ないことが予想できました。
Chrome 86による変更が追い風に
さらに2020年10月に正式版が配信されたChrome 86では、画像に関してupgrade-insecure-requests
相当の処理が自動的に入るようになりました。Windows 10でデフォルトブラウザのMicrosoft Edgeも、現在はChromiumベースのため同様の状態になっています。
これにより、とくにupgrade-insecure-requests
を設定しなくとも、既にChromeではHTTPでしか配信されていないリソースが読み込めなくなる状態が発生しており、影響はそれ以外のブラウザ(主にSafariとFirefox)にしかなく、デメリットは限定的であると判断しました。
むしろ、Chromeやその他多くのブラウザでJavaScriptやiframeが読み込めるメリットのほうが大きく、逆に設定したほうがブラウザごとの挙動が統一されることもあり、有効にすることができました。
upgrade-insecure-requestsを有効にした結果
その結果、Mixed Contentの数を10分の1以下まで減らすことができました。
TLS接続のパフォーマンスを改善
全ブログをHTTPS化することになると、TLS接続の回数が増え、パフォーマンスへの影響が考えられます。そのため、移行前にTLS接続のパフォーマンス改善を行うことにしました。
上記のように多くの独自ドメインをHTTPSで配信する必要があり、はてなブログではALBのようなマネージドサービスではなく、NginxでTLSを終端しています。このためパフォーマンス改善の余地もある状態でした。
また、2021年5月にはLet's Encryptの証明書チェーンに変更があり、チェーンが長くなってレイテンシの影響も考えられます。それに備えた対応でもありました。
証明書をRSAからECDSAに変更
最初に、証明書をRSAからECDSA(楕円曲線暗号)に変更しました。ECDSA証明書はサーバ上のパフォーマンスにおいて、RSA証明書より優れています。
一方で、比較的新しいため、接続できなくなる環境があることも考慮する必要があります。はてなブログでは2020年12月にTLS 1.0および1.1の通信を停止している関係上、ECDSA証明書に切り替えたところで閲覧できなくなる環境はほぼ存在しないことが分かっており、問題なく切り替えられると判断しました。
実際の証明書の発行に関して、はてなが提供する*.hatenablog.com
のようなドメインでは、上記の「Let's Encrypt証明書の自動更新システムを作る」という記事で詳しく説明していますが、Certbotを利用した自動更新システムを使用しています。
Certbotは、2020年12月にリリースされたバージョン1.10から、ECDSA証明書を簡単に発行できるようになっています。これを利用することで、切り替えはスムーズに行うことができました。現在、Webブラウザではてなブログに接続して鍵マークをクリックすると、次のような証明書の詳細を確認できます。
この改善により、下の図で18時前に証明書を入れ替えたのですが、このタイミングでCPU使用率が2割ほど削減できていることがわかります。
暗号スイートを整理
あわせて暗号スイートも整理しました。mozilla wikiの「Security/Server Side TLS」を参考に、TLS 1.2以上をサポートする「Intermediate compatibility (recommended)」の項目を選択しました。
実際の設定は、Mozilla SSL Configuration Generatorから同じ設定を利用することにしました。
セッションチケットでTLS接続を再利用
別の改善として、複数台で行っているTLSの終端において、接続を再利用できるようセッションチケットの設定を見直しました。これまでデフォルト設定のままだったため、セッションチケットは有効だがチケットキーがそれぞれのNginxでバラバラという状態でした。
全台でチケットキーを揃えて、次のようにTLSセッションチケットによるセッションの再開を行えるようにしました。
$ openssl s_client -reconnect -host hatenablog.com -port 443 | grep -e "\(Reuse\|New\)" depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1 verify return:1 depth=1 C = US, O = Let's Encrypt, CN = R3 verify return:1 depth=0 CN = hatenablog.com verify return:1 New, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384 Reused, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384 Reused, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384 Reused, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384 Reused, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384 Reused, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384
改善の成果
上記の改善の結果、米Qualys社のSSL Labsが提供するSSL Server Testにおいて、以前はBだったスコアを、次のようにAまで上げることができました。
NginxではTLS接続を含めたレイテンシを取得できないため、具体的に表示がどの程度速くなったかは可視化できていませんが、多少は速くなったと感じるのではないかと思います。
Nginxのバージョンアップと予期せぬ改善
TLSの改善にあわせて、Nginxもバージョンアップしました。独自ドメインをHTTPS化したとき(1.13.8)から一度も更新していなかったのですが、施策時の最新(1.21.3)まで上げました。
バージョンアップによって思わぬメリットもありました。以前は、サポートされていないTLSバージョンから接続が来た際に「失敗した」というエラーログに大量に出てノイズになる問題がありましたが、1.15.2からログレベルが変更され、そういったログがデフォルトで出力されなくなりました。
Change: a logging level of the "http request", "https proxy request",
"unsupported protocol", and "version too low" SSL errors has been
lowered from "crit" to "info".
https://nginx.org/en/CHANGES-1.16 より
これに気づいたとき「古いTLSバージョンを切ることが普通になってきたんだな」と感じました。
独自ドメイン設定時にCAAレコードも検証する
これまで何度か説明したように、独自ドメインのブログをHTTPSで配信するには、そのドメインの証明書を発行できる必要があります。逆に、適切でない独自ドメインではHTTPS配信できません。
何かの理由で証明書が発行できなかった場合、取り得る状態として以下の3つが考えられます。
- HTTPSではなく、HTTPのブログになる
- HTTPSだが、有効な証明書が存在しないブログになる
- 独自ドメインが解除され、はてなが提供するドメインのブログになる
以前は1番目の状態も考えられましたが、全部のブログをHTTPS化した際には、当然この選択肢は取れなくなります。2番目の選択肢は不具合として扱われるケースであり、回避すべきです。
つまり、適切にHTTPS配信を行うには3番目の選択肢を取ることになります。これまでも、ユーザが設定した独自ドメインのCNAMEレコードやAレコードが正しくない場合には、既に3番目の動作になっていました。
しかし、最近利用が増えているCAA(Certification Authority Authorization)レコードについては検証できていませんでした。CAAレコードが適切でないドメインは「新規に設定した際にHTTPSにできない」か「一度発行した証明書が更新できないまま有効期限を迎える」という状態になってしまいます。過去のお問い合わせでも、そういったブログが実際に存在していました。
これはブログの閲覧者にも良くない状態であり、システム上もブログはHTTPSであるという中途半端な状態になります。これを回避するため、独自ドメインの設定時にCAAレコードも検証するようにしました(以下のようにヘルプにも追記しています)。
▶ はてなブログを独自ドメインで利用する - はてなブログ ヘルプ
DNSの設定によっては証明書の再発行を何回もリトライしてしまう状態だったこともあり、ブログ側で事前に検証することで、Let's Encrypt側のレートリミットに当たらないようにする目的もありました。
キャッシュ削除でHTTPS移行の速度を上げる
ここまでHTTPS移行の実施前に設定したことなどを説明してきましたが、ここで実際にHTTPS移行する際に工夫したことを紹介します。
Varnishキャッシュ削除のジョブがボトルネックに
移行作業の初期には、対象となるブログのHTTPS化が終わったら、そのブログがHTTPのときのキャッシュを削除するため、1つ1つジョブをキューに追加しては実行していました。ここでいうキャッシュとは、リバースプロキシとアプリケーションの間に導入したVarnishです。
しかし、前述のように移行対象のブログは数百万件あるため、そのまま愚直にジョブを追加した場合、キューが詰まってしまう懸念がありました。キャッシュを削除するジョブの実行が遅れたり、それ以外の優先度が低いジョブも遅れてしまったりします。
HTTPからHTTPSへの設定変更自体はすぐ終わるので、キャッシュを破棄するジョブの処理速度に律速され、これがボトルネックになっていました。対策として、もうHTTPのブログはキャッシュを止めてしまうことも考えましたが、もしHTTPのブログに大量のアクセスがあったら、サービス全体の負荷につながってしまいます。
一括で削除し続けるシンプルな手法を採用
そこで、キャッシュをジョブで破棄するのではなく、キャッシュ削除のタグにブログのスキームを事前に追加しておき、HTTPのブログだけを対象に移行作業の裏で数秒ごとに一括でキャッシュを削除し続けるというシンプルな手法を採用しました (Varnishのxkeyモジュールを利用しています)。
これはHTTPのブログがHTTPSのブログに比べて数が少なく、アクセス数も平均して比較的少ないブログが多いことが事前に分かっていたために採用できたことです。
そもそもキャッシュしない選択とあまり変わらないようにも思えますが、キャッシュしない場合は変更をリリースして移行作業を素早く行わないと、大量アクセスに弱いタイミングがどうしても生まれてしまいます(移行作業に不備があってやり直した場合などはなおさらです)。
採用したシンプルな方式では、移行作業をしないときには通常通りキャッシュから返し続けることができ、変更をリリースするタイミングなどによらず自由に作業できることがメリットでした。デメリットとして、移行からキャッシュ破棄まで数秒のタイムラグがありますが、そもそもジョブキューを使った場合も同様のラグはあるので、ほぼ無視できると考えました。
さらに、移行作業が進んでHTTPのブログの数が少なくなるとキャッシュ削除される対象も減っていくので、後半になればなるほど影響も少なくなるというメリットもあります。これにより、実際の移行作業を数時間で終わらせることができました。
こういったキャッシュをうまく考慮した作戦の背景には、2020年に行ったキャッシュ改善の一環で行いやすくなったこともあります。詳細は次の記事を参照してください。
移行するブログ数をBigQueryで把握
最後に、HTTPS配信に関連した変更作業などではなく、移行にあたって対象となるブログの数を素早く把握するため、BigQueryを活用した事例を紹介します。
ここ数年で社内のデータウェアハウスが整備されてきており、はてなブログのデータもBigQueryですぐ集計できるようになっています。この開発者ブログに掲載されている「はてなで働くエンジニアにアンケート」シリーズでも、#14でブログの事例が、はてなブックマークの事例が#16でそれぞれ紹介されています。
ブログによって移行手段が異なることもあるので、独自ドメインのHTTPのブログの数など特定の条件ですぐ集計できるのは、移行手段を考える上でかなり役立ちました。
移行するはてなブックマークの数を把握する
中でも一番活用できたのは、はてなブックマークのデータウェアハウスとの連携です。
HTTPSに移行する際に、はてなブックマーク上のエントリーもHTTPSに移行するという作業が必要になっていました。はてなブックマークとはてなブログのシステムは完全に独立しており、これまでHTTPS化する際にはブックマークされているどうかに関わらず、移行ジョブを投機的に追加するフローになっていました。
しかし、今回の移行対象は数百万ブログあり、エントリ数は数千万といったスケールです。同様のフローで急速に移行してしまうと、はてなブックマーク側で障害が発生する懸念がありました。そこで、本当にブックマークの移行が必要なエントリーがいくつあるかなどを、BigQueryを使ってブックマークされたことのあるブログやエントリーを引くことで、把握しました。
その結果、ブックマーク移行が必要なブログは数千件で、ブックマークされたことのある記事は数万件というオーダーであることがわかり、移行対象としてもBigQueryから出力したリストを使い、問題なく短時間でブックマーク移行も終わらせることができました。
まとめとおまけ
はてなブログがHTTPS化を始めた4年前と比べると、世の中でより多くのサイトが既にHTTPS対応されており、Chromeの画像自動アップグレードを始めとして、HTTPS化への後押しをたくさん感じることができ、思ったよりスムーズに全ブログのHTTPS化を進めることができました。
また、BigQueryの活用やキャッシュをうまく考慮した移行作業など、社内で整備されてきた技術を活用して素早く移行を実施することができ、手持ちの技術をアップデートすることは大事だと実感することが多かったという印象でした。
おまけ1: Let's Encryptのルート証明書の対応
はてなブログは、社内で最もLet's Encryptを利用しているサービスで、他のサービスからリクエストを受けるサービスでもあります。このため、2021年9月末をもってLet's Encryptの古いルート証明書が期限切れになる問題では、影響を大きく受けることが想定されました。
特に古いOpenSSLから接続できなくなるという問題は他人事ではなく、はてなフォトライフを始めとして古くから社内にあるサービスには、まだOpenSSL 1.0.2がインストールされたDebian 8で動いているものもありました。
それがはてなブログやその他サイトへ接続できなくなるサービス継続上のリスクや、構築時にCDNからリソースを取得できない(つまりは新規構築できない)リスクがあることを次のように社内に広く周知し、いくつかは実際にOSのアップデートなども行いました。
情報源としては、コミュニティのAPI Announcementsカテゴリや、スケジュールは公式で用意されたACME API Eventsカレンダーを利用しました。
その結果、周囲の協力もあって、2021年9月30日のDST Root CA X3の有効期限を迎えたタイミングで、影響はほぼありませんでした。
おまけ2: 今年もLet's Encryptに寄付を実施!
はてなブログでは、ECDSA証明書に移行したり利用範囲を全ブログに拡大したりしたこともあり、Let's Encryptの利用がより広がっています。そのため、今年も運営団体であるInternet Security Research Group(ISRG)に寄付を実施しました。過去の寄付は下記の通りで、これで4年目になります。
- 2018年: はてなブログのHTTPS化実施に伴い, Let's Encryptへの寄付を実施しました
- 2019年: 今年もLet's Encryptへの寄付を実施しました
- 2020年: 今年もLet's Encryptへの寄付を実施しました
今後もはてなでは、OSSやそれを支援するコミュニティ・団体に対して、今回のような寄付を含むさまざまな形で支援を実施していきます。