Webhook解説:署名付きペイロードとリトライ戦略
ユーザーのエンドポイントにWebhookを送信するAPIを構築したとします。しかし、そこには古典的な問題が待ち受けています。ペイロードが改ざんされていないことをどう保証するのか?エンドポイントがダウンしたらどうなるのか?重複処理をどう防ぐのか?この記事では、署名付きペイロードと堅牢なリトライ戦略で、これらの課題に正面から取り組みます。
Webhookに署名が必要な理由
Webhookは、イベントを他のシステムに通知するHTTPコールバックです。公共のインターネット上を送信されるため、傍受やなりすましの脆弱性があります。署名がなければ、悪意のある攻撃者が偽のWebhookをユーザーに送信し、意図しないアクションを引き起こす可能性があります。共有シークレットでペイロードに署名することで、真正性と完全性が保証されます。
署名付きペイロードの仕組み
最も一般的なアプローチは、SHA-256を使用したHMAC(Hash-based Message Authentication Code)です。送信者と受信者は秘密鍵を共有します。送信者はペイロード(場合によってはタイムスタンプも)に対して署名を計算し、ヘッダーに含めます。受信者は署名を再計算し、受信したものと比較します。
ステップバイステップ:ペイロードへの署名
- ペイロードの作成:イベントデータをJSONにシリアライズします。署名は正確なバイト列に対して計算されるため、一貫したフォーマット(余分な空白がないなど)を確保してください。
- タイムスタンプの追加:リプレイ攻撃を防ぐため、ペイロードまたはヘッダーにUnixタイムスタンプを含めます。受信者は数分以上古いリクエストを拒否できます。
- HMACの計算:秘密鍵を使用して、ペイロード文字列(またはタイムスタンプ + ペイロード)のHMAC-SHA256を生成します。
- 署名の添付:
X-Signatureのようなヘッダーで送信します。
Pythonでの例を示します:
import hmac
import hashlib
import json
import time
secret = b'your_shared_secret'
payload = {"event": "user.created", "data": {"id": 123}}
body = json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time()))
signed_content = timestamp + '.' + body
signature = hmac.new(secret, signed_content.encode(), hashlib.sha256).hexdigest()
# Send headers: X-Signature: signature, X-Timestamp: timestamp
署名の検証
受信側では、以下を行う必要があります:
- ヘッダーからタイムスタンプと署名を抽出する。
- リプレイ攻撃を防ぐため、タイムスタンプが古すぎる場合(例:5分以上)は拒否する。
- 同じ方法で署名を再計算する。
- タイミング攻撃を避けるため、定数時間関数を使用して比較する。
Node.jsでの検証例:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
if (!signature || !timestamp) return false;
if (Date.now() / 1000 - timestamp > 300) return false; // 5 min
const body = JSON.stringify(req.body);
const signed = timestamp + '.' + body;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
リトライ戦略の設計
署名があっても、ネットワークの問題、サーバーエラー、タイムアウトによりWebhookは失敗する可能性があります。堅牢なリトライ戦略により、受信者に過負荷をかけることなく配信を保証します。
主な考慮事項
- 冪等性:受信者は重複イベントを適切に処理する必要があります。冪等性キー(例:イベントID)を使用して重複を排除します。
- 指数バックオフ:受信者が回復する時間を与えるため、遅延を増やしながらリトライします(例:1秒、5秒、30秒、5分、30分)。
- 最大リトライ回数:制限(例:5回)を設定し、その後Webhookを失敗としてマークし、場合によってはユーザーに警告します。
- タイムアウト:各試行に合理的なタイムアウト(例:10秒)を設定します。
リトライの実装
ほとんどのWebhook送信者は、リトライをスケジュールするためにキューシステム(例:Redis、RabbitMQ、またはデータベース)を使用します。以下は簡略化したフローです:
- イベントトリガー時に、ペイロードとターゲットURLを含むWebhookジョブをキューに入れます。
- ワーカーがジョブを取得し、配信を試みます。
- レスポンスステータスが2xxの場合、成功としてマークします。
- そうでない場合、最大試行回数まで指数バックオフでリトライをスケジュールします。
- 最大試行回数後、失敗としてマークし通知します。
リトライスケジュールの例:
| 試行 | 遅延 |
|---|---|
| 1 | 即時 |
| 2 | 1分 |
| 3 | 5分 |
| 4 | 30分 |
| 5 | 2時間 |
Webhookセキュリティのベストプラクティス
- HTTPSの使用:盗聴を防ぐため、常にTLS経由でWebhookを配信します。
- シークレットのローテーション:共有シークレットを定期的にローテーションし、ローテーション中は複数のアクティブなシークレットをサポートします。
- URLの検証:Webhook URLがHTTPSであり、内部IPを指していないことを確認します(SSRFを防ぐため)。
- ログと監視:デバッグと監査のために、配信試行と失敗のログを保持します。
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには、受信者が送信者の真正性とペイロードが改ざんされていないことを検証できる暗号署名(通常はHMAC)が含まれています。
失敗したWebhookを何回リトライすべきですか?
万能の答えはありませんが、一般的なアプローチは指数バックオフで3〜5回のリトライです。イベントの重要度と受信者の容量を考慮してください。
Webhook署名にJWTを使用できますか?
はい、JWTは使用できますが、より複雑です。HMACはシンプルで、ほとんどのユースケースで十分です。JWTは、署名自体にクレームや有効期限を含める必要がある場合に便利です。
Webhookペイロードをテストする準備はできましたか?送信前にJSONペイロードを検証して整形するには、JSON Formatterをご利用ください。