APIの「蛇口」を閉めろ:レート制限が単なる「混雑緩和」ではない理由
現場でインシデント対応をしていると、若手エンジニアからよくこんな相談を受ける。「APIのレート制限(Rate Limiting)って、サーバー負荷を下げるための運用的な施策ですよね?」と。
残念ながら、それは半分正解で、半分は致命的に甘い。レート制限は、アプリケーションを守るための「セキュリティの防波堤」そのものだ。
攻撃者は、あなたのAPIが「いくらでもリクエストを受け入れてくれる」ことを知っている。彼らはその隙を突き、辞書攻撃(Credential Stuffing)や脆弱性スキャン、そしてサービスそのものを沈黙させるDoS攻撃を仕掛けてくる。これらを食い止めるための「戦術」を、泥臭い実務の視点から解説しよう。
—
1. なぜ「IP単位」の制限だけでは不十分なのか
多くの開発者が真っ先に実装するのが、nginx や AWS WAF でのIP制限だ。しかし、現代の攻撃者はそんなもの軽々とすり抜ける。
- 分散型攻撃(Distributed DoS): 数千のボットネットを使い、IPを分散させる。
- NAT環境・共有IP: オフィスや大学、公共Wi-Fiからのアクセスを一律遮断すると、善良なユーザーまで巻き添えになる。
真にセキュアな実装を目指すなら、「APIキー」や「JWT(ユーザーID)」をキーにした二段構えの制限が必須だ。IPはあくまで「異常な過剰アクセスを即座に捨てるためのフィルター」として使い、アプリケーション側で「誰が何をしているか」を制御する。
—
2. トークンバケットアルゴリズムの真価
レート制限の実装でよく使われるのが「トークンバケットアルゴリズム」だ。
簡単に言えば、「バケツに一定間隔でトークンを貯め、リクエストが来るたびにそれを消費する。バケツが空ならリクエストを拒否する」という仕組み。
これの優れた点は、「バースト(急激なアクセス)」を許容しつつ、長期的には平均レートを超えさせない点にある。機械的な一定間隔制限よりも、UXと堅牢性のバランスが圧倒的に良い。
—
3. 実践:Node.js (Express) による堅牢な制限実装
Redisを使って、APIキーごとの制限を実装する例だ。メモリ上で完結させず、Redisを使うのが鉄則(水平スケーリングした全サーバーで制限を同期させるため)。
const rateLimit = require(‘express-rate-limit’);
const RedisStore = require(‘rate-limit-redis’);
const Redis = require(‘ioredis’);
const client = new Redis({ host: ‘redis-server’ });
const limiter = rateLimit({
windowMs: 15 60 1000, // 15分間
max: 100, // 15分間に100リクエストまで
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (…args) => client.call(…args),
}),
// 攻撃者にヒントを与えないよう、必要最小限のハンドリング
handler: (req, res) => {
res.status(429).json({
error: ‘Too Many Requests’,
message: ‘少し時間を置いてから再度アクセスしてください。’,
retryAfter: req.rateLimit.resetTime
});
},
// 認証済みユーザーの場合はIDで、未認証はIPで制限する
keyGenerator: (req) => req.user?.id || req.ip
});
app.use(‘/api/’, limiter);
—
4. インフラ層での防波堤:Nginxによる即時遮断
アプリケーションまで負荷を到達させないために、NginxでIPごとのハードリミットをかけておく。これは「攻撃を弾く」というよりも「サーバーを落ちないようにする」ための防波堤だ。
nginx.conf
1秒間に10リクエストを上限とし、バーストは20まで許容
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
# burstでスパイクを吸収、nodelayで即時判定
limit_req zone=api_limit burst=20 nodelay;
# 429ではなく403や503を返すことも検討(攻撃者にリトライの猶予を与えないため)
limit_req_status 429;
proxy_pass http://app_server;
}
}
—
5. 最後に:エンジニアが忘れてはならない「429の哲学」
最後に、一つだけ重要な助言をしたい。
「レート制限に引っかかったユーザーに対して、親切にしすぎないこと」だ。
攻撃者は429エラーを検知すると、レート制限の境界値を探るために「どのタイミングで制限がかかるか」を精密に計測する。エラーメッセージに詳細な残り秒数や内部ログを出しすぎるのは、相手にマップを渡すようなものだ。
- ログ: サーバー側には詳細なIP・ユーザーID・パスを記録する(これは攻撃検知の宝庫だ)。
- レスポンス: クライアントには「Too Many Requests」とだけ伝え、必要以上に情報を与えない。
APIの設計とは、信頼できるユーザーにはドアを大きく開け、悪意ある者にはその存在すら認識させない「高度な門番」を配置することに他ならない。
君たちが書くコードが、明日のインシデントを防ぐ。さあ、今すぐ全エンドポイントの制限設定を見直そう。これが、プロの仕事だ。
コメント