UnifyPort Webhook の署名シークレットを安全にローテーションする
UnifyPort webhook の signing_secret を交換するには、まずすべての受信インスタンスが現在のキーと新しいキーの両方を検証できるようにします。その後エンドポイントを更新し、新しい配信が新キーで検証できることを確認してから旧キーを撤去します。これはアプリケーション側で管理する移行です。公開 API はエンドポイントごとに単一の署名シークレットを提供しており、サーバー側の併用期間や無損失の切り替えを保証していません。
要点
- Webhook の署名キーと REST API キーは別々に交換します。
- 空の
signing_secretを中間状態に使わないでください。署名が無効になります。 - 一時的なキー集合は、対象エンドポイントと環境に限定します。
- 配信の再試行を、デプロイの猶予期間と考えないでください。
キーの役割と障害の境界を確認する
X-Api-Key はアプリケーションから UnifyPort へのリクエストを認証します。一方、エンドポイントの signing_secret はアプリケーションに届く配信を検証するためのものです。一方を変更しても他方は変わりません。REST 呼び出し側については、別記事の API キーローテーション手順を参照してください。
Webhook 配信リファレンスでは、X-Device-Signature は RFC 3339 形式の X-Device-Timestamp、リテラルのピリオド、生のリクエスト本文を連結した値に対する、16 進数の HMAC-SHA256 と定義されています。交換するのは HMAC キーであり、この入力やイベント形式ではありません。
受信側のキーが合わないと、正当なイベントも拒否されます。現行の配信仕様では、接続エラーと HTTP 408、429、5xx はバックオフなしで直ちに再試行され、それ以外の 4xx は再試行されません。先にキーを交換して発生した 401 が、後続のデプロイで自動的に回復するとは期待できません。503 を返しても、保守中の配信を確実に待機させるキューにはなりません。
3 つの構成で移行を計画する
以下は推奨するデプロイ順序であり、UnifyPort に組み込まれたローテーション機能ではありません。
| 段階 | エンドポイントの設定 | 受信側で検証するキー |
|---|---|---|
| 準備 | 現在のキー | 現在のキーと新キー |
| 切り替え | 新キー | 現在のキーと新キー |
| 旧キー撤去 | 新キー | 新キーのみ |
開始前に、エンドポイント ID、URL、状態、イベント購読、再試行設定、受信インスタンス、変更責任者を記録します。キーの値はシークレット管理基盤に保存し、変更記録には載せません。独立した新キーを生成し、通常の保護されたデプロイ経路で配布します。
この作業では URL、購読、再試行設定を変えないでください。受信先の移転と認証の変更を同時に行うと、原因の切り分けが難しくなります。旧キーの漏えいが疑われる場合は通常の併用を行わず、インシデント対応として切り替えてください。漏えいしたキーを引き続き受け付ける限り、リスクも続きます。可用性とデータ照合の方針を明確にする必要があります。
送信側を変える前に全受信インスタンスを準備する
信頼できるルート設定に、小さな一時的キー集合を持たせます。未検証の provider や account_id、独自に想定したキーバージョンヘッダーでキーを選んではいけません。公開されている配信ヘッダーには署名キーの識別子がありません。
次のサンプル関数は、最初の一致で終了せず、すべての候補を検証します。完全な HTTP 受信サーバーでも、実行済みのテスト結果でもありません。keys は対象エンドポイント専用の空でないキー文字列の配列、maxAgeMs はアプリケーションで選ぶ有限の正の許容時間です。
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyDuringRotation({
rawBody, timestamp, signature, keys, maxAgeMs,
}) {
if (!Array.isArray(keys) || keys.length === 0 ||
keys.some(key => typeof key !== 'string' || key.length === 0) ||
!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) {
throw new Error('Invalid webhook verification configuration');
}
if (!Buffer.isBuffer(rawBody) || typeof timestamp !== 'string' ||
typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) {
return false;
}
const signedAt = Date.parse(timestamp);
if (!Number.isFinite(signedAt) ||
Math.abs(Date.now() - signedAt) > maxAgeMs) return false;
const supplied = Buffer.from(signature, 'hex');
let matches = 0;
for (const key of keys) {
const expected = createHmac('sha256', key)
.update(timestamp + '.').update(rawBody).digest();
matches |= Number(timingSafeEqual(supplied, expected));
}
return matches !== 0;
}
これらの HMAC と比較処理は Node.js Crypto リファレンスに記載されています。生のバイト列を保持し、署名のないリクエストを拒否してください。署名なしで受け付ける代替処理は追加しません。検証後にペイロードを検査し、永続化します。HMAC のリプレイ対策ガイドでは、署名が一致しても鮮度チェックと重複処理対策が必要な理由を説明しています。
各受信デプロイで、現行キー、新キー、誤ったキー、改変された本文、署名欠落、古いタイムスタンプをテストします。これは推奨する受け入れテストであり、読者の環境で実行済みという意味ではありません。
署名を無効にせずエンドポイントを更新する
まず Get webhook endpoint で現在の設定を取得します。次に Update webhook endpoint の PATCH /v1/webhook-endpoints/{endpoint_id} を、REST API キーで認証して呼び出します。
確認した現在の URL、有効状態、購読、再試行設定を保った更新リクエストを作り、signing_secret に新キーを設定します。リファレンスの説明用サンプルにある空のシークレットや無効状態を、そのまま本番の切り替えに使わないでください。空の値は署名の無効化であり、自動ローテーションの指定ではありません。
設定を再取得し、signing_enabled: true と、その他の設定が変わっていないことを確認します。このフラグだけでは、どのキーが使用中かは分かりません。接続済みのメッセージングアカウントに管理されたテストメッセージを送り、新しい message.received 配信が新キーで検証され、永続化され、想定した内部処理に入るまで追跡します。キーをログに出したり、顧客の内容をテストに使ったりしないでください。
更新結果が不明確なら、両キーの検証を維持したまま設定を確認し、新しい配信をテストします。PATCH を送信したという理由だけで旧キーを撤去してはいけません。
待機時間を推測せず、撤去条件を決める
公開リファレンスには、キュー内の配信が旧署名設定を保持するかどうかや、ローテーションの最大併用時間は記載されていません。retry_policy.max_attempts は再試行回数であって、猶予秒数ではありません。無停止を約束する前に、不明な処理中配信の挙動を UnifyPort に確認してください。
全インスタンスの配備完了、新キーでの新しい配信、受信エラーの観測、合意した処理中データの扱いを撤去条件にします。運用メトリクスには機密を含まない検証バージョンのラベルだけを使います。単に配信が来ていないことは、旧キーの配信がなくなった証明ではありません。
条件を満たしたら、全受信インスタンスとデプロイ設定元から旧キーを削除します。隔離されたテストで旧キーが拒否されることを確認します。過去のキーを永久に受け付けず、併用の終了を明確な運用判断として決めてください。
ロールバックが必要で旧キーがまだ信頼できる場合は、エンドポイント設定と受信側キー集合を合わせて戻します。送信側が新キーを使っている間に、旧キー専用の受信側へ戻してはいけません。漏えいが疑われるキーは復元しません。
適用範囲と制約
UnifyPort の非公式インターフェースは、LINE を含む対応メッセージングアカウントのイベントを正規化します。この手順が保護するのは UnifyPort から受信サービスへの引き渡しです。LINE や Telegram のネイティブ認証情報、ログインセッション、API キーを変更するものではありません。
認証済みイベントを永続化してから 2xx を返します。通常イベントの再試行はイベント ID で重複排除できますが、WhatsApp の conversation.history は仕様に従ったメッセージ単位のマージが必要です。トップレベル ID だけで全体を除外してはいけません。UnifyPort は REST のメッセージ履歴読み取り API や、取り逃したペイロードの確実な再送を提供していないため、切り替え時の欠落を自動回復できるとは説明できません。
FAQ
1 つのエンドポイントに署名シークレットを 2 つ設定できますか?
公開仕様の signing_secret は単一です。ここで説明した二重検証はアプリケーション側の処理であり、API の複数キー設定ではありません。
一時的に署名を無効にしてもよいですか?
無効にしないでください。署名を有効に保ち、認証情報がない場合は拒否します。空のシークレットは署名ヘッダーをなくすだけで、移行機能ではありません。
旧キーを何分間受け付けるべきですか?
共通の公式時間はありません。配備と配信の確認、処理中の挙動、リスクに基づき、期限のある撤去判断を設定してください。
次のステップと参考資料
Update webhook endpoint を確認し、本番変更の前に隔離環境で 3 つの構成をリハーサルしてください。
確認日:2026-09-27。
メッセージ連携を安定したプロダクトパイプラインへ。
まずは 1 つの API で送信を始め、標準イベントですべての inbound メッセージを業務システムへ戻しましょう。