Skip to content

Add pre-connection hook (onHostSelected) to upstream HTTP filters #43924

@fl0Lec

Description

@fl0Lec

Title: Add pre-connection hook (onHostSelected) to upstream HTTP filters

Problem Statement

When using the Dynamic Forward Proxy (DFP) HTTP filter with sub_cluster_config mode, the save_upstream_address option does not work. This prevents the envoy.rbac.matchers.upstream_ip_port RBAC matcher from functioning because it depends on the filter-state being populated.
Additionally, the upstream_ip_port RBAC matcher is only functional when using DFP in dns_cache_config mode — any other cluster type (static, EDS, strict_dns, or DFP with sub_cluster_config) cannot use IP-based RBAC at all today.

Security Concern

The Envoy DFP documentation explicitly warns about security risks and recommends RBAC protection:

⚠️ Warning: Servers operating dynamic forward proxy in environments where either client or destination are untrusted are subject to confused deputy attacks.

Malicious clients may exploit the proxy to access localhost, link-local addresses, cloud metadata servers, or private networks.

Recommended mitigations: Protect dynamic forward proxy servers through network firewalls, default-deny RBAC, container or kernel networking restrictions.

However, using sub_cluster_config mode prevents implementing RBAC-based SSRF protection because the upstream_ip_port matcher cannot function without the filter-state being set.

Technical Details

save_upstream_address only works with dns_cache_config:

The DFP filter calls addHostAddressToFilterState() only in the DNS cache code paths:

In sub_cluster_config mode, the filter only loads the cluster (proxy_filter.cc:340-346) and never calls addHostAddressToFilterState().

upstream_ip_port matcher requires filter-state — reads from filter-state and returns false if not set (upstream_ip_port.cc:37-39):

auto address_obj = info.filterState().getDataReadOnly<StreamInfo::UpstreamAddress>(
    StreamInfo::UpstreamAddress::key());
if (address_obj == nullptr) {
  return false;  // Always returns false if filter-state not set!
}

Why It Doesn't Work Today

Mode When Address is Resolved Filter-State Set? RBAC Works?
dns_cache_config At HTTP filter layer (decodeHeaders) ✅ Yes ✅ Yes
sub_cluster_config At cluster load balancer layer (later) ❌ No ❌ No

Proposed Solution: Pre-Connection Hook for Upstream HTTP Filters

Extend the UpstreamCallbacks interface to add an onHostSelected(host) hook that fires before the upstream connection is initiated.

Interface Change (envoy/http/filter.h)

virtual void onHostSelected(Upstream::HostDescriptionConstSharedPtr host) {
  UNREFERENCED_PARAMETER(host);
  // Default: no-op (backward compatible)
}

Proposed PR

#43764

Next Steps

  1. Create an Upstream RBAC Filter (envoy.filters.http.upstream_rbac) using onHostSelected() to check host IP against CIDR rules
  2. Deprecate envoy.rbac.matchers.upstream_ip_port in favor of the new filter
  3. Future Cleanup: remove save_upstream_address, envoy.stream.upstream_address, and upstream_ip_port matcher

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementFeature requests. Not bugs or questions.triageIssue requires triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions