ブログ ·

固定 IP で Webhook を送る方法:NAT ゲートウェイ・プロキシ・中継サーバーと費用

取引先が送信元 IP で受信を制限しているときに、Webhook を固定の IP から送る方法をまとめます。NAT ゲートウェイ、フォワードプロキシ、中継サーバーの構成と月額の目安、署名と併用する理由、IP を公開してよい理由まで。

取引先に Webhook を送ろうとしたら「送信元の IP アドレスを教えてください。ファイアウォールで許可します」と言われることがあります。クラウドやサーバーレスで動くアプリは、送信元の IP がデプロイやスケールのたびに変わるので、そのままでは答えられません。この記事では、送信元を固定する 3 つの構成と費用、そして固定 IP と署名をどう組み合わせるかを書きます。

よくある場面

cybozu developer community に、2026 年 4 月にこんな質問が投稿されています(WebhookでのKintone外部連携時のIPアドレス)。kintone から Chatwork へ Webhook で連携しようとしたら、会社の IP 制限のポリシーで「通知を送るシステム側の固定 IP アドレスの情報が必要」と言われた、というものです。回答では、cybozu.com の IP アドレスは範囲で公開されているが、範囲指定なので受け側によっては難しい、として「中継サーバーを挟むしか方法はないような気がします」と答えられています。

送る側が SaaS の場合も同じです。SendGrid・Backlog・New Relic などは、送信元 IP についての FAQ やお知らせを出しています。KOMOJU のドキュメントは送信元の IP を 8 つ載せたうえで、予告なく変わることがあるので IP での制限は勧めず、署名で検証するよう書いています(KOMOJU)。一方 Stripe は Webhook の送信元 IP を 15 個公開し、変更は 7 日前にメーリングリストで知らせるとしています(Stripe)。

受信を IP で絞ることが社内の決まりになっている会社では、送る側が「署名があるので IP 制限は要りません」と伝えても通らないことがあります。送る側で固定の IP を用意できれば、この話し合いがいりません。

構成 1:NAT ゲートウェイ

アプリを VPC のプライベートサブネットに置き、外向きの通信を NAT ゲートウェイ経由にします。NAT ゲートウェイに固定の IP(AWS なら Elastic IP)を付ければ、外から見た送信元はその IP になります。

AWS の東京リージョン(ap-northeast-1)の料金は、2026-09-27 に AWS の料金表(Price List API)で確かめた値で次のとおりです。

項目 単価 月額(730 時間)
NAT ゲートウェイ 0.062 USD / 時間 約 45.3 USD
NAT ゲートウェイのデータ処理 0.062 USD / GB 通信量による
パブリック IPv4 アドレス 0.005 USD / 時間 約 3.7 USD

冗長化のために NAT ゲートウェイを 2 つ置き、IP も 2 つにすると、通信量を除いて月 約 98 USD です。これにデータ転送の料金が加わります。

向いているのは、送信の処理がすでに AWS の VPC の中で動いている場合です。Vercel や Cloudflare Workers のように VPC の外で動くアプリからは、この構成は使えません。

出典: Amazon VPC の料金、AWS Price List API(ap-northeast-1、2026-09-01 発効の単価)

構成 2:フォワードプロキシ

固定 IP を持つサーバーにフォワードプロキシ(HTTP CONNECT)を立て、Webhook の送信だけをそこ経由にします。アプリ側は、送信の HTTP クライアントにプロキシを設定するだけです。

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),
});

プロキシには、Stripe が公開している HTTP CONNECT プロキシの Smokescreen があります。Webhook 配信の OSS の Convoy は、Smokescreen を包んだ mole を使う方法をブログで案内しており、台数を増やすときはプロキシを複数台置いてロードバランサーの後ろに並べる、としています(Convoy のブログ)。

費用はサーバー代と固定 IP の代金です。自分で持つものは、プロキシの冗長化、OS とプロキシの更新、そして後で書く SSRF の対策です。

構成 3:中継サーバー

アプリから中継サーバーへ「この URL にこの本文を送って」と依頼し、中継サーバーが固定 IP から送って結果を返す構成です。フォワードプロキシとの違いは、中継サーバーが送信の中身を知っていることです。宛先の検査、打ち切りの時間、応答の記録を中継サーバー側で決められます。

Fly.io はアプリ単位の固定の送信元 IP を持てます。fly ips allocate-egress --app <アプリ名> -r nrt で東京(nrt)に IPv4 と IPv6 の組が割り当てられ、IPv4 は 1 つ月 3.60 USD です(Fly.io のドキュメント)。マシンを作り直しても IP は変わりません。

中継サーバーは、たとえば次のように作ります。アプリとの間は共有の鍵の HMAC で認証し、古い依頼は断ります。

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 のサーバーは、取引先のファイアウォールに「信頼できる送信元」として登録されます。同時に、送信先の URL は利用者が登録するものです。ここに社内のアドレスを指す URL を登録されると、固定 IP のサーバーが社内のサービスへリクエストを送ってしまいます。freee が Webhook を開発したときの記事でも、利用者が入れた URL から社内の API を叩かれないかが一番の論点になっています(freee Developers Hub)。

入れておく検査は次のとおりです。

  • 登録のときに、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 の Webhook の記事も、名前解決で得た IP アドレスを直接使って送る形で SSRF を防いでいます(NTT ドコモビジネス Engineers' Blog)。

署名と併用する理由

IP の許可は「どの経路から来たか」を絞るもので、「誰が送ったか」「途中で書き換えられていないか」までは確かめません。共有のプロキシや NAT の IP は、同じサービスを使うほかの利用者も使っています。受け側は IP で絞ったうえで、署名で送り主と本文を検証するのが確実です。Stripe のドキュメントも、IP の許可リストと署名の確認の両方を使うよう書いています(Stripe)。

取引先には、IP と一緒に署名の検証方法を渡します。Standard Webhooks の形式で署名していれば、各言語のライブラリで検証できます。

IP を公開してよい理由

送信元の IP アドレスは秘密の情報ではありません。HTTPS のリクエストは TCP の接続を往復で確立してから送るので、他人が送信元の IP を偽って最後まで届けることは現実的にできません。IP を知られても、その IP から来たように見せかけて受け側に届けることはできません。Stripe が Webhook の送信元 IP を一覧で公開しているのもそのためです。

公開するときは次の点を決めておきます。

  • 一覧の置き場所: ドキュメントと管理画面の両方に載せる。機械で読める JSON もあると、取引先がファイアウォールの設定を自動で更新できる。
  • 変更の知らせ方: Stripe は 7 日前にメーリングリストで知らせています。
  • 2 つ以上の IP: 1 台の障害で送れなくならないように。取引先には最初から全部を許可してもらう。

Webhook Admin での扱い

Webhook Admin では、送信先ごとに「固定 IP から送る」を選べます。Starter プラン(月 3,000 円)から使えます。

  • 固定 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 ゲートウェイ(AWS 東京、2 ゾーン) 約 98 USD+通信量 送信の処理がすでに VPC の中にある
フォワードプロキシ(自前) サーバー代+IP 代 既存の HTTP クライアントにプロキシを足すだけで済ませたい
中継サーバー(Fly.io など) IPv4 1 つ 3.60 USD+マシン代 サーバーレスから送る、宛先の検査や記録も中継で持ちたい
配信サービスの固定 IP 各サービスの料金による 冗長化と SSRF の対策も含めて任せたい

取引先の IP 制限のために中継サーバーを作って保守する代わりに、Webhook Admin なら Starter プラン(月 3,000 円)から固定の送信元 IP を使えます。https://app.webhookadmin.com/signup