call_webhook SSRF blocklist covers 100.64.0.0/10 (CGNAT), making webhooks unusable for Tailscale-based self-hosters
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
Workflow `call_webhook` runs SSRF protection that rejects private and reserved address ranges, including `100.64.0.0/10`. That range is CGNAT, which is what Tailscale assigns to every node. The practical effect is that a self-hosted relay physically cannot reach any service on the operator's own tailnet, so `call_webhook` is unusable for a whole class of deployment rather than merely restricted.
To be clear up front: **the SSRF policy itself is correct** and I'm not asking for it to be weakened. #4884 discusses the same protection for local commands and treats the policy as right, which I agree with. The ask is for an operator-controlled allowlist.
## Why CGNAT is different from the other blocked ranges
Blocking RFC1918 and loopback is uncontroversial — those are ambient, attacker-guessable, and mean different things on every host. `100.64.0.0/10` in a Tailscale deployment is not ambient: addresses are assigned by a coordination server, reachable only inside an authenticated mesh the operator controls, and stable per-node.
For self-hosters, that range *is* the internal network. Tailscale is probably the single most common way people wire self-hosted services together now. So this isn't "webhooks are restricted in some edge case" — for a tailnet-based deployment, `call_webhook` cannot reach anything the operator actually runs.
## Made worse by the failure being silent
#5122 reports `call_webhook` steps never delivering with no error surfaced, and names restricted egress as one cause. Combined, an operator on a tailnet gets a workflow step that silently does nothing. Nothing in the UI or logs says "this destination is blocked by SSRF policy," so the natural conclusion is that webhooks are broken, not that they're deliberately refusing.
Even without any policy change, surfacing *"destination blocked by SSRF policy"* as the step's error would save people a lot of time.
## What would help
1. **An operator-configurable destination allowlist** — an env var of CIDRs or hostnames that `check_ssrf` permits. Nothing exists for this in `.env.example` today. Defaulting to empty keeps current behaviour for everyone who doesn't set it, so it's opt-in and the safe default is unchanged.
2. **Failing loudly** — report a blocked destination as an explicit error on the workflow step rather than a silent non-delivery, per #5122.
Option 1 alone would unblock this entirely for self-hosters, and it puts the decision with the person who owns both ends of the connection.
## Possibly related
- #4884 — allowlisted local commands; cites `check_ssrf` rejecting private/reserved addresses (lines 745–766) and proposes out-of-band registration. Different remedy, same underlying tension.
- #5122 — `call_webhook` silently not delivering, restricted egress named as a cause.
- #4198 — private/VPN relay URLs rejected by mobile pairing validation. Different code path, same philosophy applied to self-hosters.
## Environment
Self-hosted `buzz-relay` (`ghcr.io/block/buzz:main`, image built 2026-08-09). All internal services reachable only over Tailscale (`100.64.0.0/10`).
Contributor guide
Research direction
Start at the call_webhook SSRF protection and the check_ssrf entry point, then inspect .env.example for existing configuration patterns. Add an opt-in destination allowlist with the current empty default and surface blocked destinations as explicit workflow-step errors; completion should preserve existing blocking behavior when unset.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100