Skip to content

feat: block private addresses for webhook destinations - #1176

Open
alexluong wants to merge 4 commits into
mainfrom
feat/block-private-destination-addresses
Open

alexluong wants to merge 4 commits into
mainfrom
feat/block-private-destination-addresses

Conversation

@alexluong

Copy link
Copy Markdown
Collaborator

Destination URLs come from tenants, and Outpost stores the response body on each attempt. A tenant can point a webhook at the operator's internal network (cloud metadata endpoints, admin services, databases on private IPs) and read what comes back. This adds a switch that keeps webhooks on public addresses. Off by default, since many deployments deliver to services on their own network.

With DESTINATIONS_WEBHOOK_BLOCK_PRIVATE_ADDRESSES=true, a webhook only connects to publicly routable addresses or to IPs and ranges listed in DESTINATIONS_PRIVATE_ADDRESS_ALLOWLIST. The check runs on the socket right before each connect, against the address actually being dialed, so a DNS answer that changes after creation (DNS rebinding) is still caught. Create and update check the URL as well and reject it early.

  1. New internal/netguard package (standard library only): the IANA special-purpose address ranges, an allowlist of IPs/CIDRs, CheckURL for a pre-flight check, and a DialContext (a standard net.Dialer) whose Control hook refuses any address that isn't allowed. Addresses embedded in IPv4-mapped, NAT64 and 6to4 IPv6 forms are judged by the IPv4 address inside.
  2. destregistry.HTTPClientConfig gets a Guard option. Without a forward proxy, every connection goes through the guard and HTTP_PROXY/HTTPS_PROXY are ignored. With a forward proxy, the proxy's own address isn't checked; the target URL is checked before each request.
  3. Webhook destinations (default and Standard Webhooks mode) use the guard when the switch is on. A URL that fails the check returns 422 (config.url must resolve to a public address), including when it doesn't resolve. A refused delivery is a failed attempt with code address_not_allowed, retried like any other failure.

Notes:

  • The allowlist sits at the destinations level so other destination types can reuse it.
  • Every redirect hop opens its connection through the guard, so a redirect to a private address is refused too.
  • Forward proxy: the proxy resolves the host name again, so rebinding through the proxy is still possible. The config row says to make the proxy refuse private addresses as well.
  • An invalid allowlist entry (host name, /0, host bits set) fails startup, also while blocking is off.

Docs: two config table rows.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant