ブログ ·
固定 IP で Webhook を送る方法:NAT ゲートウェイ・プロキシ・中継サーバーと費用
取引先が
取引先に
よくある場面
cybozu developer community に、
送る
受信を
構成 1:NAT ゲートウェイ
アプリを
AWS の
| 項目 | 単価 | 月額 |
|---|---|---|
| NAT ゲートウェイ | 0.062 USD / 時間 | 約 45.3 USD |
| NAT ゲートウェイの |
0.062 USD / GB | 通信量に |
| パブリック IPv4 アドレス | 0.005 USD / 時間 | 約 3.7 USD |
冗長化の
向いているのは、
出典: Amazon VPC の
構成 2:フォワードプロキシ
固定 IP を
import { ProxyAgent, fetch } from 'undici';
const proxy = new ProxyAgent(process.env.WEBHOOK_PROXY_URL!); // 例: http://user:pass@proxy.internal:4750
await fetch(endpointUrl, {
method: 'POST',
headers,
body,
dispatcher: proxy,
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
});プロキシには、
費用は
構成 3:中継サーバー
アプリから
Fly.io はfly ips allocate-egress --app <アプリ名> -r nrt で
中継サーバーは、
import { createHmac, timingSafeEqual } from 'node:crypto';
import http from 'node:http';
const SECRET = process.env.RELAY_SECRET!;
http.createServer(async (req, res) => {
const ts = String(req.headers['x-relay-timestamp'] ?? '');
const sig = String(req.headers['x-relay-signature'] ?? '');
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.writeHead(401).end();
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
const raw = Buffer.concat(chunks).toString('utf8');
const expected = createHmac('sha256', SECRET).update(`${ts}.${raw}`).digest('base64');
if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.writeHead(401).end();
}
const { url, headers, body } = JSON.parse(raw);
// ここで宛先を検査してから送る(次の節)
const r = await fetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(15_000) });
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ status: r.status }));
}).listen(8080);固定 IP の送信で必ず入れる SSRF の対策
固定 IP の
入れておく
- 登録の
ときに、 https だけ・ポートは 443 などに 限る・IP を 直接書いた URL と .internalなどの社内向けの 名前を 断る。 - 送る
直前に 名前を 引き、 返ってきた アドレスが 1 つでも プライベート (10/8、 172.16/12、 192.168/16) 、 ループバック、 リンクローカル (169.254/16、 クラウドの メタデータの アドレスを 含む) 、 CGNAT (100.64/10) 、 IPv6 の ULA・リンクローカルなどに 当たれば 送らない。 - 検査を
通った IP に 直接つなぐ。 名前を 引き直すと、 1 回目と 2 回目で 別の IP を 返す DNS リバインディングで 検査を すり 抜けられます。 TLS の SNI と ホスト名の 検証は 元の ホスト名で 行う。 - リダイレクトを
追わない。
SkyWay の
署名と併用する理由
IP の
取引先には、
IP を公開してよい理由
送信元の
公開する
- 一覧の
置き場所 : ドキュメントと管理画面の 両方に 載せる。 機械で 読める JSON も あると、 取引先が ファイアウォールの 設定を 自動で 更新できる。 - 変更の
知らせ方 : Stripe は7 日前に メーリングリストで 知らせています。 - 2 つ以上の
IP : 1 台の障害で 送れなくならないように。 取引先には 最初から 全部を 許可して もらう。
Webhook Admin での扱い
Webhook Admin では、
- 固定 IP の
送信は、 決まった 1 つの IPv4 から 行います。 アドレスは 管理画面と ドキュメントに 載せています。 - 送る
直前に 名前を 引いた IP を すべて 検査し、 社内向けの アドレスが 1 つでも あれば 送りません。 リダイレクトは 追いません。 送信元が IPv4 なので、 送信先には IPv4 の アドレス (A レコード) が 要ります。 - 署名
(Standard Webhooks) と 再送、 失敗の 記録は、 固定 IP の 送信でも 同じです。 送信の 記録には、 どちらの IP から 送ったかが 残ります。
API でfixed_ip を
curl -X POST https://api.webhookadmin.com/v1/endpoints \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"consumer_id":"con_...","url":"https://partner.example.jp/webhooks","fixed_ip":true}'構成の選び方
| 構成 | 月額の |
向いている |
|---|---|---|
| NAT ゲートウェイ |
約 98 USD+通信量 | 送信の |
| フォワードプロキシ |
サーバー代+IP 代 | 既存の |
| 中継サーバー |
IPv4 1 つ 3.60 USD+マシン代 | サーバーレスから |
| 配信サービスの |
各サービスの |
冗長化と |
取引先の