[{"data":1,"prerenderedAt":757},["ShallowReactive",2],{"content-query-9qBQzcZ6Tj":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"plan":11,"faq":12,"body":25,"_type":750,"_id":751,"_source":752,"_file":753,"_stem":754,"_extension":755,"sitemap":756},"/features/verify-webhook-signatures","features",false,"","Verify Webhook Signatures Before They Reach Your Network","Check HMAC signatures, JWTs and basic auth on Webhook Relay's edge, before a webhook is forwarded. Forged or tampered requests get a 401 and never reach your servers.","feature","All plans",[13,16,19,22],{"q":14,"a":15},"Where does signature verification happen?","On Webhook Relay's input, before the webhook is forwarded. A function attached to the input checks the signature and stops forwarding when it doesn't match, so a forged request never reaches your agent, your network or your server.",{"q":17,"a":18},"Which providers can I verify?","Any provider that signs webhooks with an HMAC, such as GitHub, Stripe, Shopify, Slack, PagerDuty or Square, by changing the header name and prefix in the function. Inputs also support JWT verification and basic auth for senders that use those instead.",{"q":20,"a":21},"Where is the signing secret stored?","In the function's config variables, read with cfg:GetValue. It stays in your Webhook Relay account and is not exposed in the function code or forwarded to your destination.",{"q":23,"a":24},"Does my application still need to verify the signature?","It can, and headers reach your destination unchanged, so existing checks keep working. Verifying at the edge adds a layer in front: unsigned traffic is rejected before it touches your network, and every rejection is visible in the bucket logs.",{"type":26,"children":27,"toc":745},"root",[28,53,60,99,467,505,511,660,666,718,739],{"type":29,"tag":30,"props":31,"children":32},"element","p",{},[33,36,42,44,51],{"type":34,"value":35},"text","Most webhook endpoints check signatures in application code, after the request has already reached a server on your network. Webhook Relay can check them ",{"type":29,"tag":37,"props":38,"children":39},"strong",{},[40],{"type":34,"value":41},"before forwarding",{"type":34,"value":43},". A forged, replayed or tampered request gets a ",{"type":29,"tag":45,"props":46,"children":48},"code",{"className":47},[],[49],{"type":34,"value":50},"401",{"type":34,"value":52}," at the edge and never reaches your agent, your private network or your application.",{"type":29,"tag":54,"props":55,"children":57},"h2",{"id":56},"how-it-works",[58],{"type":34,"value":59},"How it works",{"type":29,"tag":61,"props":62,"children":63},"ol",{},[64,70,82,87],{"type":29,"tag":65,"props":66,"children":67},"li",{},[68],{"type":34,"value":69},"A provider sends a signed webhook to your bucket's public URL.",{"type":29,"tag":65,"props":71,"children":72},{},[73,75,80],{"type":34,"value":74},"A ",{"type":29,"tag":37,"props":76,"children":77},{},[78],{"type":34,"value":79},"function",{"type":34,"value":81}," attached to the input recomputes the signature with your secret and compares it with the header.",{"type":29,"tag":65,"props":83,"children":84},{},[85],{"type":34,"value":86},"If it matches, the webhook is forwarded, to a public URL or through an outbound-only agent into your private network.",{"type":29,"tag":65,"props":88,"children":89},{},[90,92,97],{"type":34,"value":91},"If it doesn't, the function sets a ",{"type":29,"tag":45,"props":93,"children":95},{"className":94},[],[96],{"type":34,"value":50},{"type":34,"value":98}," and stops forwarding. Nothing else happens, and the rejection is visible in the bucket logs.",{"type":29,"tag":100,"props":101,"children":102},"code-block",{},[103],{"type":29,"tag":104,"props":105,"children":109},"pre",{"className":106,"code":107,"language":108,"meta":7,"style":7},"language-lua shiki shiki-themes github-dark","local crypto = require('crypto')\n\n-- Recompute the signature with the secret from the function's config variables\nlocal sig, err = crypto.hmac('sha256', r.RequestBody, cfg:GetValue(\"WEBHOOK_SECRET\"))\nif err then error(err) end\n\n-- GitHub sends \"sha256=\u003Chex>\" in X-Hub-Signature-256\nif \"sha256=\" .. sig ~= r.RequestHeader[\"X-Hub-Signature-256\"] then\n  r:SetResponseStatusCode(401)\n  r:SetResponseBody(\"invalid signature\")\n  r:StopForwarding()\n  return\nend\n","lua",[110],{"type":29,"tag":45,"props":111,"children":112},{"__ignoreMap":7},[113,158,168,178,259,293,301,310,368,398,428,450,459],{"type":29,"tag":114,"props":115,"children":118},"span",{"class":116,"line":117},"line",1,[119,125,131,136,142,147,153],{"type":29,"tag":114,"props":120,"children":122},{"style":121},"--shiki-default:#F97583",[123],{"type":34,"value":124},"local",{"type":29,"tag":114,"props":126,"children":128},{"style":127},"--shiki-default:#E1E4E8",[129],{"type":34,"value":130}," crypto ",{"type":29,"tag":114,"props":132,"children":133},{"style":121},[134],{"type":34,"value":135},"=",{"type":29,"tag":114,"props":137,"children":139},{"style":138},"--shiki-default:#79B8FF",[140],{"type":34,"value":141}," require",{"type":29,"tag":114,"props":143,"children":144},{"style":127},[145],{"type":34,"value":146},"(",{"type":29,"tag":114,"props":148,"children":150},{"style":149},"--shiki-default:#9ECBFF",[151],{"type":34,"value":152},"'crypto'",{"type":29,"tag":114,"props":154,"children":155},{"style":127},[156],{"type":34,"value":157},")\n",{"type":29,"tag":114,"props":159,"children":161},{"class":116,"line":160},2,[162],{"type":29,"tag":114,"props":163,"children":165},{"emptyLinePlaceholder":164},true,[166],{"type":34,"value":167},"\n",{"type":29,"tag":114,"props":169,"children":171},{"class":116,"line":170},3,[172],{"type":29,"tag":114,"props":173,"children":175},{"style":174},"--shiki-default:#6A737D",[176],{"type":34,"value":177},"-- Recompute the signature with the secret from the function's config variables\n",{"type":29,"tag":114,"props":179,"children":181},{"class":116,"line":180},4,[182,186,191,195,200,205,209,214,219,225,230,235,240,245,249,254],{"type":29,"tag":114,"props":183,"children":184},{"style":121},[185],{"type":34,"value":124},{"type":29,"tag":114,"props":187,"children":188},{"style":127},[189],{"type":34,"value":190}," sig, err ",{"type":29,"tag":114,"props":192,"children":193},{"style":121},[194],{"type":34,"value":135},{"type":29,"tag":114,"props":196,"children":197},{"style":127},[198],{"type":34,"value":199}," crypto.",{"type":29,"tag":114,"props":201,"children":202},{"style":138},[203],{"type":34,"value":204},"hmac",{"type":29,"tag":114,"props":206,"children":207},{"style":127},[208],{"type":34,"value":146},{"type":29,"tag":114,"props":210,"children":211},{"style":149},[212],{"type":34,"value":213},"'sha256'",{"type":29,"tag":114,"props":215,"children":216},{"style":127},[217],{"type":34,"value":218},", r.",{"type":29,"tag":114,"props":220,"children":222},{"style":221},"--shiki-default:#B392F0",[223],{"type":34,"value":224},"RequestBody",{"type":29,"tag":114,"props":226,"children":227},{"style":127},[228],{"type":34,"value":229},", ",{"type":29,"tag":114,"props":231,"children":232},{"style":221},[233],{"type":34,"value":234},"cfg",{"type":29,"tag":114,"props":236,"children":237},{"style":127},[238],{"type":34,"value":239},":",{"type":29,"tag":114,"props":241,"children":242},{"style":138},[243],{"type":34,"value":244},"GetValue",{"type":29,"tag":114,"props":246,"children":247},{"style":127},[248],{"type":34,"value":146},{"type":29,"tag":114,"props":250,"children":251},{"style":149},[252],{"type":34,"value":253},"\"WEBHOOK_SECRET\"",{"type":29,"tag":114,"props":255,"children":256},{"style":127},[257],{"type":34,"value":258},"))\n",{"type":29,"tag":114,"props":260,"children":262},{"class":116,"line":261},5,[263,268,273,278,283,288],{"type":29,"tag":114,"props":264,"children":265},{"style":121},[266],{"type":34,"value":267},"if",{"type":29,"tag":114,"props":269,"children":270},{"style":127},[271],{"type":34,"value":272}," err ",{"type":29,"tag":114,"props":274,"children":275},{"style":121},[276],{"type":34,"value":277},"then",{"type":29,"tag":114,"props":279,"children":280},{"style":138},[281],{"type":34,"value":282}," error",{"type":29,"tag":114,"props":284,"children":285},{"style":127},[286],{"type":34,"value":287},"(err) ",{"type":29,"tag":114,"props":289,"children":290},{"style":121},[291],{"type":34,"value":292},"end\n",{"type":29,"tag":114,"props":294,"children":296},{"class":116,"line":295},6,[297],{"type":29,"tag":114,"props":298,"children":299},{"emptyLinePlaceholder":164},[300],{"type":34,"value":167},{"type":29,"tag":114,"props":302,"children":304},{"class":116,"line":303},7,[305],{"type":29,"tag":114,"props":306,"children":307},{"style":174},[308],{"type":34,"value":309},"-- GitHub sends \"sha256=\u003Chex>\" in X-Hub-Signature-256\n",{"type":29,"tag":114,"props":311,"children":313},{"class":116,"line":312},8,[314,318,323,328,333,338,343,348,353,358,363],{"type":29,"tag":114,"props":315,"children":316},{"style":121},[317],{"type":34,"value":267},{"type":29,"tag":114,"props":319,"children":320},{"style":149},[321],{"type":34,"value":322}," \"sha256=\" ",{"type":29,"tag":114,"props":324,"children":325},{"style":121},[326],{"type":34,"value":327},"..",{"type":29,"tag":114,"props":329,"children":330},{"style":127},[331],{"type":34,"value":332}," sig ",{"type":29,"tag":114,"props":334,"children":335},{"style":121},[336],{"type":34,"value":337},"~=",{"type":29,"tag":114,"props":339,"children":340},{"style":127},[341],{"type":34,"value":342}," r.",{"type":29,"tag":114,"props":344,"children":345},{"style":221},[346],{"type":34,"value":347},"RequestHeader",{"type":29,"tag":114,"props":349,"children":350},{"style":127},[351],{"type":34,"value":352},"[",{"type":29,"tag":114,"props":354,"children":355},{"style":149},[356],{"type":34,"value":357},"\"X-Hub-Signature-256\"",{"type":29,"tag":114,"props":359,"children":360},{"style":127},[361],{"type":34,"value":362},"] ",{"type":29,"tag":114,"props":364,"children":365},{"style":121},[366],{"type":34,"value":367},"then\n",{"type":29,"tag":114,"props":369,"children":371},{"class":116,"line":370},9,[372,377,381,386,390,394],{"type":29,"tag":114,"props":373,"children":374},{"style":221},[375],{"type":34,"value":376},"  r",{"type":29,"tag":114,"props":378,"children":379},{"style":127},[380],{"type":34,"value":239},{"type":29,"tag":114,"props":382,"children":383},{"style":138},[384],{"type":34,"value":385},"SetResponseStatusCode",{"type":29,"tag":114,"props":387,"children":388},{"style":127},[389],{"type":34,"value":146},{"type":29,"tag":114,"props":391,"children":392},{"style":138},[393],{"type":34,"value":50},{"type":29,"tag":114,"props":395,"children":396},{"style":127},[397],{"type":34,"value":157},{"type":29,"tag":114,"props":399,"children":401},{"class":116,"line":400},10,[402,406,410,415,419,424],{"type":29,"tag":114,"props":403,"children":404},{"style":221},[405],{"type":34,"value":376},{"type":29,"tag":114,"props":407,"children":408},{"style":127},[409],{"type":34,"value":239},{"type":29,"tag":114,"props":411,"children":412},{"style":138},[413],{"type":34,"value":414},"SetResponseBody",{"type":29,"tag":114,"props":416,"children":417},{"style":127},[418],{"type":34,"value":146},{"type":29,"tag":114,"props":420,"children":421},{"style":149},[422],{"type":34,"value":423},"\"invalid signature\"",{"type":29,"tag":114,"props":425,"children":426},{"style":127},[427],{"type":34,"value":157},{"type":29,"tag":114,"props":429,"children":431},{"class":116,"line":430},11,[432,436,440,445],{"type":29,"tag":114,"props":433,"children":434},{"style":221},[435],{"type":34,"value":376},{"type":29,"tag":114,"props":437,"children":438},{"style":127},[439],{"type":34,"value":239},{"type":29,"tag":114,"props":441,"children":442},{"style":138},[443],{"type":34,"value":444},"StopForwarding",{"type":29,"tag":114,"props":446,"children":447},{"style":127},[448],{"type":34,"value":449},"()\n",{"type":29,"tag":114,"props":451,"children":453},{"class":116,"line":452},12,[454],{"type":29,"tag":114,"props":455,"children":456},{"style":121},[457],{"type":34,"value":458},"  return\n",{"type":29,"tag":114,"props":460,"children":462},{"class":116,"line":461},13,[463],{"type":29,"tag":114,"props":464,"children":465},{"style":121},[466],{"type":34,"value":292},{"type":29,"tag":30,"props":468,"children":469},{},[470,472,478,480,486,488,494,496,503],{"type":34,"value":471},"For other providers, change the header name and the prefix. PagerDuty uses ",{"type":29,"tag":45,"props":473,"children":475},{"className":474},[],[476],{"type":34,"value":477},"X-PagerDuty-Signature",{"type":34,"value":479}," with ",{"type":29,"tag":45,"props":481,"children":483},{"className":482},[],[484],{"type":34,"value":485},"v1=",{"type":34,"value":487},". Shopify sends a base64 digest in ",{"type":29,"tag":45,"props":489,"children":491},{"className":490},[],[492],{"type":34,"value":493},"X-Shopify-Hmac-Sha256",{"type":34,"value":495},". The ",{"type":29,"tag":497,"props":498,"children":500},"a",{"href":499},"/docs/webhooks/auth/hmac/",[501],{"type":34,"value":502},"HMAC guide",{"type":34,"value":504}," walks through creating the function, storing the secret and testing it.",{"type":29,"tag":54,"props":506,"children":508},{"id":507},"other-ways-to-authenticate-senders",[509],{"type":34,"value":510},"Other ways to authenticate senders",{"type":29,"tag":512,"props":513,"children":514},"table",{},[515,539],{"type":29,"tag":516,"props":517,"children":518},"thead",{},[519],{"type":29,"tag":520,"props":521,"children":522},"tr",{},[523,529,534],{"type":29,"tag":524,"props":525,"children":526},"th",{},[527],{"type":34,"value":528},"Method",{"type":29,"tag":524,"props":530,"children":531},{},[532],{"type":34,"value":533},"Use it when",{"type":29,"tag":524,"props":535,"children":536},{},[537],{"type":34,"value":538},"Guide",{"type":29,"tag":540,"props":541,"children":542},"tbody",{},[543,565,586,608,638],{"type":29,"tag":520,"props":544,"children":545},{},[546,552,557],{"type":29,"tag":547,"props":548,"children":549},"td",{},[550],{"type":34,"value":551},"HMAC signature (function)",{"type":29,"tag":547,"props":553,"children":554},{},[555],{"type":34,"value":556},"The provider signs the body: GitHub, Stripe, Shopify, Slack, PagerDuty, Square and most SaaS",{"type":29,"tag":547,"props":558,"children":559},{},[560],{"type":29,"tag":497,"props":561,"children":562},{"href":499},[563],{"type":34,"value":564},"HMAC",{"type":29,"tag":520,"props":566,"children":567},{},[568,573,578],{"type":29,"tag":547,"props":569,"children":570},{},[571],{"type":34,"value":572},"JWT",{"type":29,"tag":547,"props":574,"children":575},{},[576],{"type":34,"value":577},"The sender puts a signed token in a header",{"type":29,"tag":547,"props":579,"children":580},{},[581],{"type":29,"tag":497,"props":582,"children":584},{"href":583},"/docs/webhooks/auth/jwt/",[585],{"type":34,"value":572},{"type":29,"tag":520,"props":587,"children":588},{},[589,594,599],{"type":29,"tag":547,"props":590,"children":591},{},[592],{"type":34,"value":593},"Basic auth",{"type":29,"tag":547,"props":595,"children":596},{},[597],{"type":34,"value":598},"The sender supports a username and password in the URL or header, like Grafana and Alertmanager",{"type":29,"tag":547,"props":600,"children":601},{},[602],{"type":29,"tag":497,"props":603,"children":605},{"href":604},"/docs/webhooks/auth/username-password/",[606],{"type":34,"value":607},"Username & password",{"type":29,"tag":520,"props":609,"children":610},{},[611,616,629],{"type":29,"tag":547,"props":612,"children":613},{},[614],{"type":34,"value":615},"HTTP method filter",{"type":29,"tag":547,"props":617,"children":618},{},[619,621,627],{"type":34,"value":620},"Only ",{"type":29,"tag":45,"props":622,"children":624},{"className":623},[],[625],{"type":34,"value":626},"POST",{"type":34,"value":628}," should ever be accepted",{"type":29,"tag":547,"props":630,"children":631},{},[632],{"type":29,"tag":497,"props":633,"children":635},{"href":634},"/docs/webhooks/auth/http-method/",[636],{"type":34,"value":637},"HTTP method",{"type":29,"tag":520,"props":639,"children":640},{},[641,646,651],{"type":29,"tag":547,"props":642,"children":643},{},[644],{"type":34,"value":645},"Source IP allowlist",{"type":29,"tag":547,"props":647,"children":648},{},[649],{"type":34,"value":650},"The provider publishes the IP ranges it sends from (Business and above)",{"type":29,"tag":547,"props":652,"children":653},{},[654],{"type":29,"tag":497,"props":655,"children":657},{"href":656},"/docs/webhooks/auth/ip-allowlist/",[658],{"type":34,"value":659},"IP allowlist",{"type":29,"tag":54,"props":661,"children":663},{"id":662},"why-verify-at-the-edge",[664],{"type":34,"value":665},"Why verify at the edge",{"type":29,"tag":667,"props":668,"children":669},"ul",{},[670,688,698,708],{"type":29,"tag":65,"props":671,"children":672},{},[673,678,680,686],{"type":29,"tag":37,"props":674,"children":675},{},[676],{"type":34,"value":677},"Nothing unsigned enters your network.",{"type":34,"value":679}," When webhooks are delivered through an ",{"type":29,"tag":497,"props":681,"children":683},{"href":682},"/docs/security/",[684],{"type":34,"value":685},"outbound-only agent",{"type":34,"value":687},", the check happens before the agent ever sees the request.",{"type":29,"tag":65,"props":689,"children":690},{},[691,696],{"type":29,"tag":37,"props":692,"children":693},{},[694],{"type":34,"value":695},"One place for every provider.",{"type":34,"value":697}," The secret lives in the function's config variables, not spread across services.",{"type":29,"tag":65,"props":699,"children":700},{},[701,706],{"type":29,"tag":37,"props":702,"children":703},{},[704],{"type":34,"value":705},"Visible rejections.",{"type":34,"value":707}," Every request, accepted or rejected, is in the bucket logs, which helps when a provider rotates a secret.",{"type":29,"tag":65,"props":709,"children":710},{},[711,716],{"type":29,"tag":37,"props":712,"children":713},{},[714],{"type":34,"value":715},"Your existing checks still work.",{"type":34,"value":717}," Headers reach the destination unchanged.",{"type":29,"tag":30,"props":719,"children":720},{},[721,723,729,731,737],{"type":34,"value":722},"Need to test a signature first? The free ",{"type":29,"tag":497,"props":724,"children":726},{"href":725},"/verify-webhook-signature/",[727],{"type":34,"value":728},"webhook signature verifier",{"type":34,"value":730}," checks a payload, secret and header in your browser. For the bigger picture, see ",{"type":29,"tag":497,"props":732,"children":734},{"href":733},"/solutions/",[735],{"type":34,"value":736},"security solutions",{"type":34,"value":738},": SIEM alerts, GitOps and incident automation delivered without opening a port.",{"type":29,"tag":740,"props":741,"children":742},"style",{},[743],{"type":34,"value":744},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":170,"depth":170,"links":746},[747,748,749],{"id":56,"depth":160,"text":59},{"id":507,"depth":160,"text":510},{"id":662,"depth":160,"text":665},"markdown","content:features:verify-webhook-signatures.md","content","features/verify-webhook-signatures.md","features/verify-webhook-signatures","md",{"loc":4},1794645185594]