Skip to content

feat: block private addresses for webhook destinations - #1176

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

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

Conversation

@alexluong

@alexluong alexluong commented Oct 27, 2026 •

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_BLOCK_PRIVATE_ADDRESSES=true, a webhook only connects to publicly routable addresses or to IPs and ranges listed in DESTINATIONS_PRIVATE_ADDRESS_ALLOWLIST. Create and update check the URL and reject it early. Without a forward proxy, the check also 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.

  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 warning at startup when they are set). With a forward proxy, deliveries aren't checked: the proxy resolves and connects to the target, so it is the only place that can enforce this, and the config row says to make it refuse private addresses. Checking each request in Outpost would add a DNS lookup per delivery and could still be bypassed by rebinding.
  3. Webhook destinations (both webhook modes) 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:

  • Network egress rules stay the main control. This switch covers what they can't see, such as connections to localhost, and is a defense for deployments without them.
  • The allowlist sits at the destinations level so other destination types can reuse it.
  • An invalid allowlist entry (host name, /0, host bits set) fails startup, also while blocking is off.

Docs: an "Outbound Network Access" section on the self-hosting Deployment page (when to care, network egress rules, this switch), a note in the forward proxy guide, two config table rows.

Not in this PR: the same check for other destination types that take a tenant-supplied host (SQS, EventBridge, Kinesis, Pub/Sub, RabbitMQ, Kafka, Service Bus).

🤖 Generated with Claude Code

if err != nil {
return fmt.Errorf("private address allowlist: %w", err)
}
webhookOpts = append(webhookOpts, destwebhook.WithAddressGuard(&netguard.Guard{Allowlist: allowlist}))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexluong for the other destination types where the tenant gives us an HTTP endpoint, do we want the same rule? I think we probably should. From a quick look that's queue_url on SQS and endpoint on EventBridge, Kinesis and Pub/Sub.

RabbitMQ (server_url), Kafka (brokers) and Azure Service Bus (connection_string) also take a tenant-supplied host, so the same question applies there, even though they aren't HTTP.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the same rule should cover them. I've renamed the switch to DESTINATIONS_BLOCK_PRIVATE_ADDRESSES so it isn't tied to webhooks, and I'll keep this PR to webhooks and follow up with the rest: SQS, EventBridge, Kinesis and Pub/Sub can reuse the same dialer check through their SDK's HTTP client, while RabbitMQ, Kafka and Service Bus need their own dialer hooks.

…VATE_ADDRESSES

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexbouchardd

Copy link
Copy Markdown
Contributor

Docs for self hosting should really cover this internal network thing. Are we confident this is a definitive answer? Should we recommend to enable? Should we propose networking / node level firewall instead?

alexluong and others added 9 commits October 30, 2026 16:14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stination-addresses

# Conflicts:
#	internal/destregistry/providers/destwebhook/httphelper.go
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

3 participants