はじめに
Webhook、URLプレビュー、ファイル取得機能、外部API連携など、多くのアプリケーションではユーザーや外部システムが指定したURLへアクセスする機能が存在します。
これらの機能は利便性を高める一方で、実装や運用方法によってはSSRF(Server-Side Request Forgery)のリスクを抱えることになります。
本記事では、Amazon SQS および AWS Lambda を活用した外部通信基盤の構成案と、その設計上の考慮点について紹介します。
背景
一般的なSSRF対策としては、以下のような入力値検証が挙げられます。
- localhostの拒否
- プライベートIPアドレスの拒否
- ホワイトリスト方式
- リダイレクト制御
- DNS Rebinding対策
- DNS名前解決後の接続先IPアドレスの検証
単純なURL文字列の検証だけでは不十分なケースも増えています。
例えば、ホスト名の検証時には問題がなくても、DNS名前解決後にプライベートIPアドレスへ誘導されるケースや、取得したファイルが画像を装った想定外のコンテンツであるケースも考慮する必要があります。
そのため、
- DNS解決結果の検証
- 通信直前の接続先IPアドレスの確認
- MIMEタイプ、拡張子チェック、マジックナンバー(シグネチャ)の検証
- アップロードファイルの再エンコード・再変換
などの追加対策が採用されることもあります。
これらは重要な対策ですが、URL処理を行う機能が増えるたびに同様のレビューが必要になります。
また、アプリケーションごとに実装されることで、対策レベルにばらつきが発生する可能性もあります。
そこで本記事では、
「本体アプリケーションが直接外部通信を行わないようにする」
というアプローチをご紹介いたします。
もし、上記の対策をすり抜ける想定外のリクエストが発生した場合でも、処理を重要なシステムから隔離し、アクセス先をインターネット上の公開リソースのみに限定することで、被害範囲を限定することを目指すことができます。
なお、OWASP Top 10でも、SSRF対策として「Segment remote resource access functionality in separate networks to reduce the impact of SSRF(リモートリソースへのアクセス機能を別ネットワークへ分離し、SSRFの影響を低減する)」ことが推奨されています。
本記事で紹介する構成も、この考え方をAWS上で実現する一例です。
アーキテクチャ
構成の概要は以下の通りです。
Application
|
v
Amazon SQS Standard または FIFO
|
v
AWS Lambda
|
v
Internet
アプリケーションは接続先の情報及び送信内容をAmazon SQSへ送信します。
AWS Lambdaがキューを購読し、実際の外部通信を実施します。
これにより、
- 通信処理の共通化
- アプリケーションからの分離
- 権限の最小化
が期待できます。
Lambdaによる通信の分離
外部通信を行うLambdaには、必要最小限のIAM権限のみを付与します。
このLambdaだけを専用VPCへ配置し、NAT Gateway経由でのみインターネットアクセスを許可するのも有効です。
また、アプリケーション本体は直接インターネットへ通信せず、Lambdaを経由する構成とすることで、外部通信に関する責務を一箇所へ集約しやすくなります。
新たなURLアクセス機能が追加された場合も、個別実装ではなく共通基盤を利用することで、運用負荷の削減が期待できます。
また、URLの検証などの共通的なバリデーションをLambdaへ集約することもできます。
Amazon SQS を利用する理由
アプリケーションからLambdaを直接呼び出す構成であっても、適切なネットワーク分離や権限制御が行われていれば、外部通信処理を本体システムから隔離するという目的は達成できます。
しかし、Amazon SQSを使うことで、セキュリティ上の隔離に加え、アクセス集中の吸収や障害時のバッファリング、アプリケーションと外部通信処理の疎結合化といった可用性・運用性の向上を目指すことができます。
結果の受け渡し
外部通信の結果をアプリケーションへ返却する方法としては、さまざまな選択肢があります。
例えば、
- Amazon SQS
- Amazon DynamoDB
などが考えられます。
システムの特性によっては同期レスポンスが求められる場合もありますが、非同期処理を前提とすることで通信基盤とアプリケーションの結合度を下げることができます。
Lambdaから直接本体システムのAPIをコールする構成は推奨していません。
なぜなら、外部通信を行うコンポーネントから本体システムのAPIへ到達できる状態になっていると、万が一URL検証の不備などによりSSRFが発生した際、ユーザーが指定したURLとして本体システムのAPIへアクセスされる可能性があるためです。
そのため、本体システムとの連携はSQSやDynamoDBなどの中間層を介して行い、ネットワーク的にも疎結合な構成とするのが適切です。
SQS Standard にするか SQS FIFO にするか
Amazon SQSには大きく分けて以下の2種類があります。
- SQS Standard
- SQS FIFO
前提として、順序保証が不要であれば、まずSQS Standardを検討します。
SQS Standardは高いスループットを実現できますし、料金もSQS Standardの方が安価です。
しかし、SQS Standardは、メッセージの順序は保証されません。
一方、SQS FIFOは順序保証を提供しますが、スループットには一定の制約があります。
さらに料金もSQS Standardと比べると高くなります。
そのため、どちらを選択するかは単純な性能比較ではなく、扱うデータの特性によって判断する必要があります。
例えば、イベント会場の来場人数を外部システムへ連携するケースを考えてみます。
連携する方法として、①差分方式、②絶対値方式の2種類の方式を見てみましょう。
①差分方式
来場人数の変化を差分として送信する場合、
+1人
-1人
+3人
のようなメッセージになります。
この例では加算・減算のみを扱うため、順序保証は必ずしも重要ではありません。
順序が入れ替わっても、最終的な来場人数は同じになるためです。
(※ただし、「0未満になるとエラーとする」といった業務ロジックが存在する場合は、順序によって結果が変わる可能性があります。)
しかし、欠点として、累積誤差を補正する仕組みがありません。
つまり、何らかの要因で、二重送信、送信漏れなどが発生してしまった場合、それ以降ズレ続けてしまい、何らかの介入をしない限り、補正されることはありません。
この欠点を許容出来る場合は、差分方式を採用します。
差分方式の場合は、SQS Standardでも、SQS FIFOのいずれでも可能ですが、将来的なスケールを考えるのであれば、ほぼ無制限のスループットが利用できる上に安価な、SQS Standardを採用します。
②絶対値方式
一方で、変化があるたびに、もしくは定期的に、
来場人数 = 100
来場人数 = 99
のような絶対値を送信する方式も考えられます。
この方式では、冪等性があり、一時的に送信が失敗したとしても、最終的な状態は整合します。
しかし、今度は順序性が重要になります。
例えば、
来場人数 = 100
↓
来場人数 = 99
という更新が、
来場人数 = 99
↓
来場人数 = 100
の順序で処理されると、古い状態へ戻ってしまいます。
さらに、この方式で、SQS Standardを採用すると、複数の処理が並行して実行された場合にレースコンディションが発生する可能性があります。
例えば、
処理Aが来場人数99を取得する
処理Bが来場人数100を取得する
処理Bが先に送信される
処理Aが後から送信される
という順序になった場合、外部システムには古い情報が残る可能性があります。
また、外部システム側の内部実装や処理順序までは制御できません。
そのため、送信順序を保証しつつ、外部システム側の実装に依存しない構成を目指す場合は、FIFOキューの採用を検討します。
FIFOキューは順序保証を提供する一方で、スループットに制約があります。
そのため、
- 順序性を優先するのか
- スループットを優先するのか
- 重複実行をどのように扱うのか
をシステム要件に応じて検討することが重要です。
絶対値方式では、順序が崩れると古い状態へ戻る可能性があります。
そのため、スループットよりも順序保証を優先し、FIFOキューを採用するのが適切です。
なお、最新のスループットの数値については、 https://aws.amazon.com/jp/sqs/features/ をご参照ください。
障害対応における考慮点
Amazon SQSではDead Letter Queue(DLQ)を利用できます。
DLQは障害調査や再処理を行う上で有効な仕組みですが、すべてのケースで単純な再実行が適切とは限りません。
例えば、
- 時間経過によって意味が変化するデータ
- 常に最新状態を優先すべきデータ
では、古いメッセージを再実行することで期待しない結果となる可能性があります。
たとえば、前項の絶対値方式のように最新状態のみを保持したいケースでは、過去の状態を表すメッセージを単純に再送すると、かえって古い状態へ戻してしまう可能性があります。
そのため、システムによってはDLQへの再投入ではなく、最新データを再取得して新たなキューを作成するアプローチを選択することもあります。
重要なのは、データ特性に応じて障害復旧方法を設計することです。
外部サービス障害の影響の局所化
本記事の主な目的はSSRFリスクの低減ですが、副次的な効果としてシステムの耐障害性向上も期待できます。
外部サービスとの通信では、タイムアウトや高負荷によるレスポンス遅延が発生することがあります。
アプリケーションが直接通信を行う場合、外部サービスの応答待ちによってアプリケーションサーバーのリソースが占有され、システム全体のレスポンス低下につながる可能性があります。
Amazon SQSを介した非同期処理とすることで、通信先サービスの一時的な遅延や障害が発生した場合でも、その影響を通信基盤内に閉じ込めることができます。
これにより、特定の外部サービスの問題がアプリケーション全体へ波及するリスクを低減できます。
監査と可観測性
通信処理をLambdaへ集約することで、外部通信に関するログを一元的に管理できます。
Amazon CloudWatchを利用することで、
- 通信成功率
- エラー率
- タイムアウト発生状況
- リトライ状況
- 特定ドメインへのアクセス集中
などを継続的に監視できます。
また、アプリケーションごとにログを収集する必要がなくなるため、運用面でのメリットもあります。
おわりに
外部通信機能は、多くのシステムにおいて必要不可欠な機能です。
一方で、適切な設計が行われなければSSRFをはじめとするさまざまなリスクの入り口にもなります。
Amazon SQS や AWS Lambda といったマネージドサービスを活用することで、アプリケーションから外部通信を分離し、より安全で運用しやすい基盤を目指すことができます。
入力値検証やファイル検査といった対策は引き続き重要ですが、それらだけに依存せず、万が一の際にも被害範囲を限定できる構成を検討することが重要です。
本記事が、外部通信機能の設計を検討する際の参考になれば幸いです。