0020: Webhooks run their own tunnels

On this page

Status: accepted

Context

Webhooks used to come in by sharing a whole site (0010). That ties a webhook's address to a site, offers no way to answer before the app exists, and leaves deliveries unchecked. Sharing's password can't protect a webhook, because providers can't type one, yet tunnel traffic is untrusted.

Decision

  • Tunnels. Each webhook runs its own tunnel into the existing share listener.
    • A random address is a quick tunnel (webhook:<id>) whose Host header is an internal name, <id>.webhook.leash.invalid.
    • On your domain, a named webhook gets a CNAME for its name on the computer's one named tunnel.
    • A webhook name may not equal a site name, because their domain addresses would collide.
  • Access. Tunnel requests for a webhook's host keep the IP allowlist and skip only the share password.
  • Signature check. Each delivery's signature is checked (webhooks::signature; HMAC through ring, constant-time). Optionally, a delivery that fails is answered 401 before any site sees it.
  • Forwarding. Deliveries are forwarded to their sites through the proxy as local HTTPS requests, so they never trust forwarded headers.
  • Storage. Signing secrets live in secrets/webhooks.json (0600), as 0012 does for sharing. They never go in config.toml or the API. Deliveries are kept in memory, the last 500 per webhook. Amended by 0029: deliveries are kept in Short Leash's database, the last 500 per webhook for the days chosen (14 by default), held ones until they're released.

Consequences

  • A webhook's address no longer depends on sharing a site, and Short Leash can hold deliveries, answer for them, replay them and explain failed signatures.
  • A webhook without a secret, or with rejection off, takes anything sent to its address. The screen marks such deliveries Not checked or Failed.
  • Random addresses still change on restart; your domain is the fix.
  • Every webhook on a random address is one more cloudflared process.