LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

This post is also available in the following languages. English, Korean

秒間100万件、LINEアプリへのApache Kafkaエンドツーエンド暗号化の導入記

はじめに:なぜKafkaのエンドツーエンド暗号化なのか?

LINEメッセンジャーでは、毎日数十億件のメッセージがやり取りされています。この膨大なデータはさまざまなシステムに送信されますが、その中には個人情報のように、非常に高いレベルのセキュリティが求められる機密データも存在します。LINEヤフーでは、こうした機密データを多層的なセキュリティ体制で保護しており、脅威環境の変化やサービス規模の拡大に合わせて、セキュリティレベルを継続的に強化しています。

LINEメッセンジャーのトークルーム内のメッセージには、すでにエンドツーエンド暗号化技術であるLetter Sealingが適用されています。さらに、LINEヤフーはユーザーデータに高い水準のセキュリティを適用するため、Kafkaクライアント間のエンドツーエンド暗号化(end-to-end encryption)を導入することにしました。この方式を採用することで、Kafkaブローカーに保存されるメッセージのペイロードは、プロデューサーからコンシューマーに至るまで暗号化された状態で維持されます。

この記事では、LINEヤフーがKafkaのエンドツーエンド暗号化を設計・実装し、実際のサービスに適用した経験とその結果を共有したいと思います。特に、1つのチームが発行して複数のチームのコンシューマーが消費する、最大で秒間100万件のデータがやり取りされる大規模なKafkaトピックに対し、無停止でエンドツーエンド暗号化を設計・実装・適用した経験とその成果をご紹介します。

従来のKafkaセキュリティモデルの限界

Kafkaは基本的に、以下の3つの主要なセキュリティ機能を提供してデータを保護します。

  • 通信経路の暗号化(TLS/SSL):HTTPS通信で使用されるSSLプロトコルを活用し、ブローカーとクライアント(プロデューサー、コンシューマー)間の通信チャネルを暗号化します。これにより、ネットワーク上を移動するデータの盗聴を防ぎます。
  • 認証(authentication):SASL(Simple Authentication and Security Layer)プロトコルを使用し、クライアントがブローカーにアクセスする前に身元を確認します。これにより、許可されていないユーザーによるシステムへのアクセスを防ぎます。SASLは、ID/パスワードやKerberosなど、多様な認証方式に対応しています。
  • 認可(authorization):ACL(access control list)を用いて、特定のユーザーやグループに対し、特定トピックへのメッセージの発行や消費などの操作を実行できる権限を付与または制限します。

これらの強力な基本セキュリティ機能がありますが、機密性の高いデータには追加の保護層が必要です。通信経路の暗号化や認証・認可は、データが移動するプロセスやアクセス権限の保護に重点を置いたセキュリティレイヤーであり、それぞれのレイヤーが保護する対象と範囲が異なるためです。ブローカーサーバーも厳格なアクセス制御の下で運用されていますが、これは「誰がアクセスできるか」を制御するものであり、ブローカー内に保存されたデータ自体は平文の状態で存在しています。

個人情報のように高いレベルのセキュリティが求められるデータについては、こうしたアクセス制御に加え、データ自体を暗号化する防御レイヤーも備えることが望ましいです。つまり、データを生成する時点から暗号化を行い、コンシューマーが復号するまでその状態を維持する必要があります。これが、私たちがエンドツーエンドの暗号化を導入した理由です。アクセスを制限する従来の保護体制の上に、データそのものを保護するレイヤーを追加する「多層防御(defence in depth)」戦略です。

コア設計

安全かつ効率的なKafkaのエンドツーエンド暗号化システムを構築するため、以下の主要な要件を定義しました。

  1. 機密性:メッセージはプロデューサー側で暗号化して送信し、許可されたコンシューマーのみが復号できるようにする必要があります。
  2. 拡張性:プロデューサーやコンシューマーを柔軟に追加または削除できるようにします。新しいコンシューマーをシステムに容易に追加できる柔軟な構造を備えている必要があります。
  3. パフォーマンスオーバーヘッドの最小化:暗号化および復号プロセスで発生するリソース(CPU、メモリ)の追加消費やメッセージサイズの増加を最小限に抑える必要があります。

これら3つの要件のうち「機密性」を前提として、「拡張性」と「パフォーマンス」を両立できる方向でメッセージ構造を設計しました。

レコード単位の暗号化

暗号化の設計において最初に決めるべきことは、「どの単位で暗号化を適用するか」でした。

「バッチ単位の暗号化」は、複数のレコードをバッチとしてまとめた後、ブローカーへ送信する前に暗号化する手法です。暗号化前に圧縮できるため、効率が良く、CPUオーバーヘッドが少なく、処理速度も速いです。しかし、Kafkaクライアントが提供する公式の拡張ポイント(インターセプターなど)はすべてレコード単位で動作し、バッチが組み立てられた後のタイミングには介入できません。そのため、バッチレベルで暗号化を行うには、Kafkaクライアントの内部コードを直接修正する必要があります。

一方、「レコード単位の暗号化」は、複数のレコードをバッチとしてまとめる前に、各レコードを個別に暗号化する手法です。バッチ単位の暗号化と比較すると、圧縮効率が著しく低下するため、暗号化のコストがやや高くなり、メッセージサイズもわずかに増加しますが、Kafkaの標準的な拡張ポイントを活用できるという決定的な利点があります。

レコード単位で暗号化する場合、Kafkaが公式に提供している拡張ポイント、インターセプターを活用できます。標準APIを使用することで、Kafkaのバージョンアップ時にも暗号化ロジックの安定性が担保され、既存のプロデューサーやコンシューマーのKafkaクライアントを修正することなく暗号化を適用できます。こうした理由から、最終的にレコード単位の暗号化を採用しました。

DEK-KEK構造

単一鍵方式には、鍵が漏洩した際に保護対象全体の機密性が損なわれるリスクがあります。私たちは、DEK(data encryption key)によるデータ保護の効率性と、KEK(key encryption key)による鍵管理の柔軟性を両立させるため、DEK-KEKの二重鍵構造を採用しました。

  • DEK:DEKには、プロデューサーがメッセージのペイロードを暗号化するためのAES対称鍵を採用しており、ペイロードの暗号化にはAES-GCMモードを使用します。対称鍵アルゴリズムの特性上、演算速度が非常に高速です。
  • KEK:KEKには、許可されたコンシューマーが共有するECC(楕円曲線)ベースの非対称鍵ペアを使用します。この鍵ペアはKMS(key management service)に登録して管理します。プロデューサーはKMSから公開鍵を取得してDEKを暗号化し、権限を付与されたコンシューマーはKMSから秘密鍵を取得して暗号化されたDEKを復号します。DEKの暗号化にはECIES(Elliptic Curve Integrated Encryption Scheme)方式を使用し、楕円曲線としてはsecp521r1を採用しています。

DEK-KEK構造導入による利点

1. 大規模なKafka環境におけるパフォーマンス最適化

非対称鍵(KEK)のみで大容量のペイロード全体を暗号化すると、演算コストが非常に高くなります。そのため、私たちは実際のデータ暗号化には演算速度の速い対称鍵(DEK)を使用し、処理コストの高い非対称鍵の演算は、短いDEKを暗号化する場合に限定して使用しています。これにより、高トラフィックを処理するKafka環境でも、暗号化のオーバーヘッドを最小限に抑えることができます。

2. メッセージサイズを一定に保つ(複数のコンシューマーへの対応)

コンシューマーがどれだけ増えても、プロデューサーはペイロードを一度だけ暗号化すれば済みます。メッセージヘッダーにはKEKで暗号化された短いDEKの値のみが含まれるため、メッセージ全体のサイズを一定に保ちつつ、多数のコンシューマーへデータを安全に届けることができます。

3. 暗号化権限と復号権限の分離

非対称鍵構造を活用することで、「暗号化権限」と「復号権限」を明確に分離できます。プロデューサーには公開鍵ベースの暗号化権限のみを付与し、認可されたコンシューマーにのみ秘密鍵ベースの復号権限を付与します。これにより、最小権限の原則を実現し、万が一公開鍵が外部に漏洩しても、既存のメッセージの機密性を安全に保護できます。

メッセージ構造

以下は、既存のKafkaメッセージと、E2EE適用後のメッセージ構造を比較したものです。

Kafkaの既存メッセージ構造とE2EE適用後の構造の比較

メッセージを正常に復号するには、暗号化されたデータとともに、復号に必要な情報(メタデータ)を渡す必要があります。外部ストレージ(DB、キャッシュなど)を使用すると、システムへの依存関係が複雑になるため、私たちはメッセージ自体にメタデータを含める方式を選択しました。

コンシューマーはメッセージを受信した後、メタデータを確認して、自身が復号できるKEKが存在するかどうかを判別します。その後、そのKEKを使用してDEKを復号し、最終的に暗号化されたメッセージのペイロードを復号します。

  • キー(key):メッセージのパーティションを決定するために使用される既存のキーは、そのまま維持します。
  • ヘッダー(header):Kafka 0.11.0バージョン以降で使用可能なヘッダーに、復号に必要なメタデータを格納しました。これには、「コンシューマーを識別するためのKEK ID」と「KEKで暗号化されたDEK」が含まれます。
  • ボディ(body):DEKで暗号化された最終ペイロードが入ります。

システムアーキテクチャ

前述のメッセージ構造に基づき、実際のシステムでは以下のように、プロデューサーとコンシューマーがそれぞれの役割を果たすように実装しました。

プロデューサーとコンシューマーの役割

プロデューサー:インターセプターを活用した暗号化とDEKキャッシュ

プロデューサーは、メッセージを暗号化してブローカーに送信する役割を担います。

  • DEKの生成:メッセージのペイロードを暗号化するための対称鍵(DEK)を生成します。
  • DEKの暗号化:事前に取得した公開鍵(KEK)でDEKを暗号化します。
  • メッセージの作成:暗号化されたDEKをヘッダーに、DEKで暗号化されたペイロードをボディに格納し、最終的なメッセージを完成させます。

これらの暗号化処理はすべて、Kafkaのインターセプター(interceptor)とシリアライザー(serializer)によって行われます。

  • インターセプター:ヘッダーを処理します。メッセージが送信される直前にインターセプトしてDEKを生成し、KEKでDEKを暗号化した後、その情報をヘッダーに挿入します。
  • シリアライザー:ペイロードを処理します。既存のデータシリアライザーをラップするラッパー(wrapper)形式で実装しました。1次段階として直列化されたバイナリデータを受け取り、インターセプターが準備したDEKを使用して2次段階としての暗号化を行います。
  • DEKの受け渡し:インターセプターとシリアライザーが同じ実行スレッドを共有する特性を利用して、生成されたDEKをThreadLocalに格納して渡し、処理完了後にリソースを解放します。

メッセージごとにDEKをその都度生成してKEKで暗号化するのは、リソースの無駄な消費となります。そのため、私たちはDEKを一定時間キャッシュし、その間に送信されるメッセージには同じDEKを使用するように実装しました。これにより、ほとんどのメッセージは平文データを暗号化する処理だけで済むため、プロデューサーのパフォーマンスを大幅に向上させることができます。

コンシューマー:デシリアライザーを用いた復号戦略

コンシューマーは、ブローカーから送信される暗号化されたメッセージを受け取り、復号する役割を担います。

  • KEKの取得:KEKはトピック所有者が生成し、KMSに登録します。各コンシューマーは認可プロセスを経た後、KMSから秘密鍵を取得してメッセージの復号に使用します。
  • DEKの復号:受信したメッセージのヘッダーから暗号化されたDEKを検索し、自身が保有するKEKの秘密鍵で復号します。
  • ペイロードの復号:復号されたDEKを使用して、メッセージボディに含まれるペイロードを復号します。

この復号プロセスは、デシリアライザー(deserializer)内で処理されます。デシリアライザーは、既存の逆直列化ロジックをラップする形で実装されます。メッセージを受信すると、まずヘッダーのメタデータに基づいてDEKを復号し、そのDEKでペイロードを復号した後に逆直列化を行います。

複数のプロデューサーは、それぞれ異なるDEKを使用する可能性があります。そのため、コンシューマーは暗号化されたDEKと復号されたDEKのペアをキャッシュし、同じ暗号化DEKが再度受信された場合には復号プロセスをスキップするようにしました。この戦略により、DEKの復号に要するリソースを削減し、コンシューマーの効率を高めることができました。

KMS

KEKの生成、配布、ローテーションはすべてKMSを通じて管理されます。トピック所有者が非対称鍵ペアを生成してKMSに登録すると、プロデューサーは公開鍵を、権限を付与されたコンシューマーは秘密鍵を取得して使用します。新しいコンシューマーが追加される際は、KMSに対して該当する秘密鍵へのアクセス権限を申請し、トピック所有者の承認を得て復号権限を取得します。

大規模サービスへの適用に向けた最適化戦略

設計と実装の完了後、実際のLINEサービスに適用するにあたり、3つの重要な課題がありました。それは、メッセージサイズの増加、無停止移行、運用中の鍵のローテーションです。

メッセージサイズの最小化:共有KEK(Shared KEK)の導入

エンドツーエンド暗号化システムを実際のサービスに適用する上で直面した最大の課題は、「メッセージサイズの増加問題」でした。Kafkaトピックの特性上、複数のチームのコンシューマーが存在したり、継続的に追加されたりすることがあります。このとき、コンシューマーごとに固有の鍵を割り当てると、メッセージヘッダーにコンシューマーの数だけメタデータが蓄積され、それに比例してメッセージサイズが増加します。メッセージサイズの増加は、システム全体のパフォーマンス低下を招く致命的な要因です。1つのバッチに含まれるレコード数が減少するだけでなく、ネットワーク帯域幅やCPU、メモリなどのシステムリソースの消費量が全体的に急増するためです。

特に、前述した秒間最大100万件という大規模なトラフィック環境では、コンシューマーごとに異なるKEKを使用した場合、ヘッダーサイズが急激に増加し、システム全体のパフォーマンスが低下します。また、新規コンシューマーが追加されるたびにヘッダーにメタデータが追加されるため、長期的な運用が困難な構造でした。

私たちはこの問題を根本的に解決するため、当初から複数のコンシューマーが1つのKEKを共有する方式を採用しました。共有KEKを使用すれば、コンシューマーの数に関わらず、メッセージヘッダーには1つのメタデータのみが含まれます。共有KEKは、コンシューマーごとの鍵の分離ではなく、一定のメッセージサイズを選択したトレードオフであり、これを補うためにKMSの認可と定期的な鍵のローテーションを併用しています。

  • 鍵の配布:トピック所有者が非対称鍵ペアを生成し、KMSに登録します。プロデューサーはKMSから公開鍵を、許可されたコンシューマーはKMSから秘密鍵を取得して使用します。鍵の生成と配布の責任はトピック所有者に集中します。
  • アクセス制御:コンシューマーがKMSから秘密鍵を取得するには、認可プロセスを経る必要があります。このアクセス制御が、暗号化されたデータのセキュリティ境界として機能します。
  • 鍵のローテーション:共有鍵構造のリスクを軽減するため、定期的な鍵のローテーションを義務付けます(無停止ローテーションメカニズムについては後述します)。

無停止移行

平文フォールバック

暗号化を導入する際、最大のリスクはデータの消失とサービス停止です。すべてのプロデューサーとコンシューマーを同時に移行することはできないため、暗号化メッセージと平文メッセージが共存する過渡期を安定してサポートし、乗り切らなければなりません。そのために、コンシューマーのデシリアライザーがメッセージヘッダーにおけるメタデータの有無を検知し、動作を切り替える構造を実装しました。

  • ヘッダーあり:ヘッダーが存在する場合は暗号化メッセージと判断し、ヘッダーから暗号化情報を抽出して復号した後、逆直列化を行います。
  • ヘッダーなし:ヘッダーが存在しない場合は平文メッセージと判断し、復号プロセスをスキップして、従来どおり逆直列化のみを行います。

このような平文フォールバック構造により、以下の安全な手順で移行を進めることができました。

  1. コンシューマーの先行デプロイ:フォールバックロジックを含むコンシューマーを先にデプロイし、どのような形式のメッセージが届いても処理可能な状態にします。
  2. プロデューサーの暗号化を有効化:すべてのコンシューマーの準備が整った後、プロデューサーの暗号化機能を有効にします。
  3. 検証と完了:モニタリング指標を通じて、平文メッセージの割合が0%になったことを確認し、移行を終了します。

段階的なデプロイ

コンシューマー側の準備が整っていたとしても、プロデューサー側の暗号化率を一気に100%に引き上げるのは危険です。予期せぬパフォーマンスの低下や暗号化/復号エラーに備えるため、動的な設定に基づく段階的なデプロイ戦略を採用しました。

  • 段階的な適用範囲の拡大:初期段階では、暗号化率を最低値(例:1%)に設定し、ごく一部のトラフィックにのみ暗号化を適用します。
  • 指標に基づく意思決定:CPU負荷、処理遅延(latency)、エラー率などの指標をリアルタイムで監視しながら、10%、50%、100%と段階的に適用範囲を広げていきます。
  • 即時ロールバック:異常の兆候が検知された場合は、設定値を調整するだけで、直ちに暗号化率を0%に下げ、システムをロールバックできます。

運用の安定性:無停止鍵ローテーションメカニズム

共有KEKは、複数のコンシューマーが同一の鍵を使用する構造であるため、鍵が漏洩した場合、許可されていないユーザーが過去のメッセージだけでなく、将来のメッセージまで復号できてしまうリスクが生じます。これを解決するため、定期的に鍵をローテーションすることをセキュリティ要件として定義しました。また、このローテーションはサービスを停止することなく行われなければなりません。そのために、切り替え期間中は旧鍵と新鍵が共存するメカニズムを実装しました。

無停止ローテーションメカニズム

プロデューサーのインターセプターは、KMSの公開鍵リストを一定周期でポーリング(polling)します。変更が検出されると、現在のDEKを登録されているすべての公開鍵でそれぞれ暗号化し、メッセージヘッダーに含めます。したがって、鍵の切り替え期間中は、ヘッダーに旧KEKで暗号化されたDEKと、新KEKで暗号化されたDEKが並存することになります。

コンシューマーも同様に、一定周期でKMSをポーリングし、新しい秘密鍵を自動的に反映します。コンシューマーは、ヘッダーの各項目に含まれるKEK IDを確認し、自身の鍵と一致する項目のみを選択して復号します。これにより、旧鍵を使用しているコンシューマーと新鍵を使用しているコンシューマーが、切り替え期間中でも同じメッセージを同時に処理することが可能です。

無停止ローテーションメカニズム

ローテーション手順

  1. 新KEKの登録:KMSに新しいバージョンのKEKを登録します。この時点では、旧バージョンも維持されます。
  2. プロデューサーの使用確認:約1分後、プロデューサーが新KEKを反映したかどうかを指標で確認します。旧KEKと新KEKの両方が表示されていれば正常です。この時点から、メッセージヘッダーには2つの暗号化されたDEKが含まれます。
  3. コンシューマーの新鍵切り替え確認:約1分後、コンシューマーが新KEKで復号を行っているかどうかを指標で確認します。
  4. 旧KEKの無効化:コンシューマーの新鍵への切り替えを確認してから、KMSで旧バージョンを無効にします。このステップを早めると、コンシューマーでの復号に失敗する可能性があるため、必ずステップ3の確認を経てから行います。
  5. ローテーション完了のモニタリング:プロデューサーが新KEKのみを使用していることを最終確認し、ローテーションプロセスを完了します。

検証とデプロイ

パフォーマンステスト(ベンチマークテスト)

暗号化を実際にデプロイする前に、稼働中の本番環境トピックに対して、パフォーマンスの観点からどのような影響を与えるかを定量的に測定しました。理論的な推定に頼るのではなく、実際のプロダクショントラフィック規模を再現したテスト環境でベンチマークテストを実施しました。

テスト設計

  • 環境:本番環境と同一の仕様のサーバーを使用したテスト環境を構築し、既存のトピックに影響を与えないよう、テスト専用のトピックを別途作成しました。
  • 負荷の再現:負荷テストツールを利用して、本番環境で測定された最大RPS(requests per second)および最大メッセージサイズをそのまま再現しました。
  • 測定方法:平文メッセージと暗号化メッセージをそれぞれ10分間処理させ、インスタンスごとのCPU使用量を測定し、その差を暗号化オーバーヘッドとして算出しました。
  • コンシューマーの並行性:実環境のパーティション数とスレッド数を基準に、テスト環境のパーティション数に合わせて並行性を比例的に調整し、実際と同様の負荷条件を再現しました。

テスト結果

すべての測定対象において、インスタンスあたりのCPU使用率の増加は1%未満でした。最も高いRPS(〜5,000件/秒)と数十KB規模のメッセージサイズという条件を併せ持つトピックにおいても、プロデューサーとコンシューマーの両方で、CPU使用率の増加は約1%にとどまりました。この結果を踏まえ、Kafkaの暗号化導入に伴うサーバー増設は不要という結論に至りました。

本番環境へのデプロイ

ベンチマークテストで安全性を確認した後、段階的に展開を進め、約2週間にわたり暗号化率を100%まで引き上げました。

  • 運用指標:全デプロイ期間中、暗号化/復号に関する障害通知は1件も発生しませんでした(0件)。
  • 実際の負荷:本番環境におけるリアルタイムのCPU使用率の増加幅も、予測値と同様に1%以下に抑えられました。

これにより、サービス停止やインフラコストの追加なしに、Kafkaの全区間にわたるエンドツーエンド暗号化の導入に成功しました。

おわりに

この記事では、LINEがKafkaのエンドツーエンド暗号化をどのように設計・適用したかについて紹介しました。

今回のプロジェクトを通じて構築したKafkaクライアント間エンドツーエンド暗号化システムにより、LINEメッセンジャーのデータセキュリティをさらに高いレベルへと引き上げることができました。メッセージの作成、送信、保存の全プロセスにおいてデータの機密性を確保することで、世界中のユーザーがLINEメッセンジャーをより安心して利用できる強固なセキュリティ基盤を確立しました。より安全で信頼できるメッセージングプラットフォームを構築するためのLINEヤフーの技術的挑戦は、今後も続きます。

Changgu Han

Name:Changgu Han

Description:LINEアプリのメッセージサーバーの開発を担当しています。

Hyeon Woo Jeong 

Name:Hyeon Woo Jeong 

Description:LINEアプリのメッセージサーバーの開発を担当しています。

Haruki Okada

Name:Haruki Okada

Description:全社レベルの大規模なKafkaプラットフォームを開発・運用しています。