AI時代の「自滅」を防ぐ:モデルDoS攻撃からインフラを守る鉄則
現場で死線をくぐり抜けてきた諸君、お疲れ様。
最近、LLMを組み込んだアプリケーションをローンチした後に、「急にAPIのレスポンスが極端に遅くなった」「クラウドの利用料が予算の5倍になった」という泣き言をよく耳にする。
そのほとんどが、悪意あるユーザーによる「モデルDoS攻撃(Model DoS)」だ。
従来のDoS攻撃がネットワーク帯域を狙うのに対し、モデルDoSは「GPUの演算コスト」を食いつぶす。1プロンプトで数千円のコストがかかるLLMにおいて、これを放置するのは「財布の鍵を開けて通りすがりに金を配っている」のと同義だ。今日は、この泥臭いインシデントを未然に防ぐための防衛術を伝授する。
—
1. なぜ「モデルDoS」は従来の防御をすり抜けるのか
通常のWeb攻撃であれば、WAFがIP単位で接続数を制限すれば終わりだ。しかし、モデルDoSは違う。攻撃者は、以下のような「高負荷プロンプト」を巧妙に送り込んでくる。
- 極端なトークン長: コンテキストウィンドウ上限ギリギリのゴミデータを送り込み、アテンション機構の計算量を爆発させる。
- 再帰的・複雑な推論要求: 「この論理パズルを100ステップで解け」といった、GPUを長時間占有するタスク。
- 並列リクエスト: 1つのIPから同時多発的にリクエストを飛ばし、モデルの推論キューを埋め尽くす。
これらは正常なリクエストと見分けがつきにくく、WAFの単純なレート制限だけでは「計算リソースの枯渇」までは防げない。「トークン数」「ユーザーの行動スコア」「キューの滞留時間」、この3層で防壁を築く必要がある。
—
2. 防御の要:Pythonによるトークン制限の実装例
まず、最も単純だが効果的なのが「入力トークン数の制限」だ。リクエストがモデルに到達する前に、トークン数をカウントして遮断する。
import tiktoken
from fastapi import HTTPException, Request
# モデルごとのトークン制限設定(コストとGPU負荷を考慮)
MAX_TOKENS = 4000
encoding = tiktoken.get_encoding("cl100k_base")
async def validate_token_limit(request: Request, body: str):
# プロンプトのトークン数を計算
tokens = len(encoding.encode(body))
# トークン数が閾値を超えていれば拒否
if tokens > MAX_TOKENS:
# 413 Payload Too Large を返すのが適切
raise HTTPException(
status_code=413,
detail=f"Token limit exceeded. Your prompt: {tokens}, Limit: {MAX_TOKENS}"
)
return True
—
3. レート制限の極意:ユーザー単位の「バケット制御」
IP制限だけでは、NAT配下の正当なユーザーを誤BANしてしまう。必ずログインユーザーID(またはAPIキー)単位で管理すること。
Redisを用いたトークンバケットアルゴリズムの実装が、今の現場ではデファクトスタンダードだ。
import redis
import time
# Redis接続(インフラチームと相談し、レイテンシの低いものを選ぶこと)
r = redis.Redis(host='localhost', port=6379, db=0)
def is_rate_limited(user_id: str, limit: int = 5, window: int = 60):
"""
user_idごとに、window秒間にlimit回までのリクエストを許可する
"""
key = f"rate_limit:{user_id}"
count = r.get(key)
if count and int(count) >= limit:
return True # 制限超過
# カウンタをインクリメントし、有効期限を設定
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
pipe.execute()
return False
—
4. インフラ層での防御:Nginxによる保護
アプリケーションコードだけで防御するのは「甘え」だ。万が一コードが落ちても、入り口で弾く設計にしておく。Nginxの limit_req モジュールは、特定のパス(/api/chatなど)に対して強力な防壁となる。
# nginx.conf の設定例
# 1秒間に1リクエストを許容し、バーストは5まで。user_idをキーにする
limit_req_zone $http_x_user_id zone=ai_limit:10m rate=1r/s;
server {
location /api/chat {
# 制限超過時は503を返す
limit_req zone=ai_limit burst=5 nodelay;
proxy_pass http://backend_cluster;
}
}
—
5. 最後に:セキュリティは「多層」であるべき
技術的な実装を紹介したが、最後に一つだけ忠告しておく。セキュリティに「魔法の杖」はない。
- モニタリング: DatadogやPrometheusで「推論にかかった時間(Latency)」と「トークン消費量」を可視化せよ。異常なスパイクは、攻撃の予兆だ。
- キューイング: モデルの推論を同期処理にせず、一旦メッセージキュー(RabbitMQ/SQS)に逃がすアーキテクチャに切り替えることも検討しろ。これにより、スパイクが来てもアプリ全体が落ちるのを防げる。
「動くコード」を書くことはエンジニアの基礎だが、「攻撃されても耐えうるアーキテクチャ」を設計するのがプロの仕事だ。今日紹介したコードはそのままプロダクションに投入できるレベルのものだ。さあ、今すぐ君のシステムの監視ログを確認し、防御層が不足していないか見直してほしい。
健闘を祈る。
コメント