私は、世界で最も訪問されたWebサイトの1つに取り組んだ後に下したいくつかの結論についてお話したいと思います。
このプロジェクトの作業にコンサルタントとして参加する機会がありました。 リソーストラフィックは、1か月あたり約2億人のユニークユーザーです。 このような人気は、情報セキュリティの分野で高いレベルのリスクを意味します。特に、これはさまざまなタイプの攻撃のリスクであり、最も一般的なのはDDoSです。 私は名前を付けませんが、このような攻撃がサービスのパフォーマンスに与える影響を防ぐために、幅広いソリューションを実装しています。
これらの保護システムは非常に一般的です。 これらは、境界ノード(CDN、ESI)でのコンテンツのアセンブリとマルチレベルパッシブキャッシュの使用に基づいています。
このような設計は、サービスの安定した運用を保証するのに適しています。 ただし、パッシブキャッシュを使用するように設計されたアプリケーションを作成すると、プログラマチームに多くの追加の作業負荷がかかります。 これについては、以下で詳しく説明します。
このプロジェクトに取り組んでいる間に、パッシブキャッシュと同じ利点があるが、システムの基礎となるサービスのアーキテクチャを厳密に規制しないDDoS攻撃から保護する方法を見つけました。 彼は今日議論されます。
パッシブキャッシュとは何ですか?
パッシブキャッシュを使用するサービスは、キャッシュからのみデータを読み取ることができます。 サービスは、このデータのソースについて何も知りません。
この構成では、キャッシュサポートシステムはキー値データストレージ(Redisなど)であり、メインデータソースはリレーショナルデータベース管理システム(Oracle Databaseなど)です。
アクティブなキャッシュを持つサービスは、最初にキャッシュからデータを読み取ろうとします。これが失敗した場合、メインデータソースにアクセスします。
パッシブキャッシュを使用してDDoSから保護する
パッシブキャッシュアーキテクチャを使用すると、ソースデータのソースを持つメインサービスが予期しない大量のリクエストに遭遇することはありません。 サービスに対していくつの要求が行われても、メインデータソースはメッセージキューサービスによってのみ使用され、キャッシュデータストアがいっぱいになります。
たとえば、
http://gajus.com/blog/はブログサービスです。 記事はここに投稿されます。 クライアントは、一意のインデックスを使用して個々の記事にアクセスできます。 記事のアドレスの例を次に示します。
この例では、「1」、「2」、「8」、および「9」はリソース識別子であり、ストレージ内のデータにアクセスするために使用される一意のインデックスです。
問題のブログサービスはアクティブキャッシュを使用します。 クライアントがインデックス「1」の記事を要求すると、サービスはキャッシュにアクセスして結果を返します(または、要求された記事のデータを含むレコードがキャッシュに含まれていない場合)、データベースにアクセスし、結果を受信してキャッシュにしばらく保存します。
攻撃者がHTTPリクエストの実行を伴う攻撃を組織し、インデックスが「1」の記事を取得する場合、これらのリクエストはすべてキャッシュリポジトリによって処理されます。 キーと値のストレージからデータをクエリするには、多くのリソースは必要ありません。 このようなリポジトリの検索サブシステムをオーバーロードしてシステムを攻撃するには、攻撃者は非常に深刻な機能を必要とします。
攻撃が任意の値を使用して記事識別子を作成する場合、たとえば100万から100万の範囲の値の場合、状況は大きく変わります。 これで、各リクエストは、メインデータベースにアクセスする必要があるという事実につながります。
キーと値のストレージでの検索とは異なり、リレーショナルデータベースへのクエリは非常にリソースを消費します。 要求/応答パケットが非常に多くのノードを通過する必要がある可能性は高いです。また、アプリケーションロジックを使用して応答を処理する必要があり、結果をキャッシュに保存する必要がある可能性もあります。
キーと値のストレージのスケーリングは高速で安価であり、リレーショナルデータベース管理システムのスケーリングについては言えません。
サービスがパッシブキャッシュを使用する場合、スケーリングの問題はそれに限定されます。 パッシブキャッシュを備えたアーキテクチャは、キャッシュの能力を迅速かつ便利に高めるように直接設計されています。 ただし、このようなアーキテクチャは開発を複雑にします。
パッシブキャッシュを使用するサービスの開発
パッシブキャッシュを使用するサービスを作成する場合、いくつかの要件を考慮する必要があります。
- まず、データはキャッシュからのみ読み取ることができます。
- 第二に、サービスによって要求されたデータを作成、更新、または削除する各操作の後、ソースデータウェアハウスへの要求のキューに対応するタスクを配置する必要があります。
- 第三に、データを作成、更新、または削除する各タスクが主ストレージのデータに対して実行された後、サービスはキャッシュの対応するセクションを更新するタスクをキューに入れる必要があります。
上記の制限を考慮して、システムの各CRUD操作を実装する必要があります。
開発プロセス中、タスクキューが存在するため、何らかの操作を実行する要求から結果を受信するまでの時間が長くなります。 これにより、開発とテストのプロセスが遅くなります。 さらに、プログラマーは、キャッシュ内の古いデータが原因で発生する可能性のある特定のエラーについて知る必要があります。
一方、アクティブキャッシュを使用するアプリケーションの開発中、プログラマーは作業中にキャッシュを無効にするだけで、メインデータウェアハウスへの任意のリクエストを非常に高速に処理できます。
パッシブキャッシュを使用してシステムを開発することはより困難ですが、セキュリティが最初になると、通常はそのような困難に注意を払いません。 ただし、これはDDoS攻撃に対するすべての保護方法が確実に多大な努力を必要とすることを意味するものではありません。 上記では、記事の識別子を列挙することによるブログサービスへの攻撃の例を見てきました。 リソース識別子を予測不能にすることにより、このような攻撃の影響を軽減できます。
リソース識別子の署名
アクティブキャッシュを備えたシステムが上記の攻撃を受けやすい理由は、攻撃者がリソース識別子を簡単に構築できるためです。 識別子が、GraphQL APIのようにbase64 GUIDでエンコードされた数値ID(この例のように)であるか、ほとんどのドキュメント指向データベースのようにUUIDであるかに関係なく、サーバーが受信するときの問題は要求、要求されたリソースが存在するかどうかはわかりません。 見つける唯一の方法は、キャッシュまたはメインデータソースのいずれかを呼び出して、応答を待つことです。 サーバーが、何にも頼らずに、要求されたリソースが存在するかどうかを判断できるようにするために、リソース識別子に署名できます。
署名により、システムのパフォーマンスに重大な影響を与えることなく、リクエストが正しく形成されているかどうかを確認できます。 リソース識別子が署名されている場合、攻撃者は公開IDの限られたセットの一部ではない識別子を要求することはできません。
このように機能します。サービスはリクエストを受信し、リソース識別子の復号化を試みます。 彼が成功すると、復号化された値を使用して、要求されたレコードが検索されます。 識別子を復号化できない場合、要求処理は完了します。GraphQLリソース識別子を作成するときにこのアプローチを使用します。 特に、GraphQLクエリをリダイレクトするプロキシは、最初にリソースIDが有効かどうかを確認します。
SGUID
署名付きGUID、または
sguidはNode.jsの協定
であり、署名済み識別子を作成および検証する手順を示しています。
toSguid
を使用し
toSguid
識別子に
toSguid
できます。 署名された識別子を確認して開くには、
fromSguid
コマンドを使用します。 次のようになります。
import { fromSguid, InvalidSguidError, toSguid, } from 'sguid'; const secretKey = '6h2K+JuGfWTrs5Lxt+mJw9y5q+mXKCjiJgngIDWDFy23TWmjpfCnUBdO1fDzi6MxHMO2nTPazsnTcC2wuQrxVQ=='; const publicKey = 't01po6Xwp1AXTtXw84ujMRzDtp0z2s7J03AtsLkK8VU='; const namespace = 'gajus'; const resourceTypeName = 'article'; const generateArticleSguid = (articleId: number): string => { return toSguid(secretKey, namespace, resourceTypeName, articleId); }; const parseArticleSguid = (articleGuide: string): id => { try { return fromSguid(publicKey, namespace, resourceTypeName, articleSguid).id; } catch (error) { if (error instanceof InvalidSguidError) {
識別子の署名に加えて、Sguidは名前空間とリソースタイプ識別子を使用するように設計されています。 これにより、識別子のグローバルな一意性が保証されます。
SguidはEd25519
公開鍵暗号システムを使用し
ます 。 結果の署名は、base64 URLエンコードを使用してエンコードされます。
このアプローチの欠点は、人々が使用するのに不便な識別子です:
pbp3h9nTr0wPboKaWrg_Q77KnZW1-rBkwzzYJ0Px9Qvbq0KQvcfuR2uCRCtijQYsX98g1F50k50x5YKiCgnPAnsiaWQiOjEsIm5hbWVzcGFjZSI6ImdhanVzIiwidHlwZSI6ImFydGljbGUifQ
さらに、開発プロセスを過度に複雑にすることなく、OSIモデルのアプリケーションレベルで実行されるDDoS攻撃に対するスケーラブルな保護。
まとめ
ここで説明する手法は、通信チャネルのオーバーフローを狙った攻撃にはまったく役に立たないことに注意してください。 さらに、キャッシュがすべての意味のあるリクエストのデータを保存できる場合にのみ有効です。 しかし、このような制限にもかかわらず、これはサーバーキャッシュミスのために設計された攻撃から保護するための注目に値するアプローチです。
さらに、サイバー攻撃から保護するためには、攻撃者の観点から攻撃の結果がどのように見えるかが重要であることを覚えておく必要があります。 ここで説明するアプローチを使用して効率的に設計されたシステムを「突破」するためには、リソース識別子を反復処理するために大きな力が必要です。 おそらく、攻撃者は単にそのようなことを期待しておらず、システムが彼の行動に反応しないことを確認します(可能性の限界でうまく機能するかもしれませんが)、彼はすでに可能なすべてを試したと判断し、攻撃を停止します。
また、DDoS攻撃から身を守るにはどうすればよいですか?