はじめに
「Webhookを送ったのか送っていないのかわからない」——at-least-once配信・HMAC署名・指数バックオフリトライでWebhook配信を確実にする設計をClaude Codeに生成させる。
CLAUDE.mdに設計ルールを書く
## Webhook配信保証設計ルール
- at-least-once配信(最低1回は必ず届ける)
- HMAC-SHA256でペイロードに署名(ユーザーごとに異なる秘密鍵)
- X-Webhook-Signatureヘッダー: sha256=hex
- タイムスタンプ検証: 5分以上古いリクエストは拒否(リプレイ攻撃防止)
- 配信タイムアウト: 10秒
- リトライ: 1m→5m→15m→1h→4h→12h(6回、合計約17時間)
- 6回失敗でオーナーへアラート
生成される実装(抜粋)
// HMAC署名(タイムスタンプを含めてリプレイ攻撃を防止)
export function signWebhookPayload(payload, secret, timestamp) {
const signedPayload = timestamp + '.' + payload;
const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
return { signature: 'sha256=' + signature };
}
// 署名検証(受信側)
export function verifyWebhookSignature(payload, timestamp, signature, secret) {
const ts = parseInt(timestamp, 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5分以上古い
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(timestamp + '.' + payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// 配信(タイムアウト10秒 + 失敗時リトライスケジュール)
async deliver(deliveryId) {
const timestamp = Math.floor(Date.now() / 1000);
const { signature } = signWebhookPayload(delivery.payload, delivery.endpoint.secret, timestamp);
const response = await fetch(delivery.endpoint.url, {
method: 'POST',
headers: {
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp.toString(),
'X-Webhook-Delivery': delivery.id,
},
body: delivery.payload,
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
const delayMs = RETRY_DELAYS_MS[delivery.attempt]; // 1m→5m→15m→1h→4h→12h
await deliveryQueue.add('deliver', { deliveryId }, { delay: delayMs });
}
}
まとめ
- CLAUDE.md にat-least-once配信・HMAC-SHA256署名・5分タイムスタンプ許容・6回リトライ(最大17時間)を明記
- 署名ペイロードにタイムスタンプを含める ——同じpayloadで古い署名を再利用するリプレイ攻撃を防止
- タイムアウト10秒 で受信側の重い処理をブロックしない——応答だけ確認し実際の処理は受信側が非同期で行う
- 6回失敗で配信失敗確定+オーナーアラート ——サイレント消失させず、受信エンドポイントの障害を検出してユーザーに通知
セキュリティ設計のレビューは **Security Pack(¥1,480)* の /security-check で確認できます。*
みょうが (@myougatheaxo) — ウーパールーパーのVTuber。
Top comments (0)