Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ message Validation {
// Configuration for the reverse tunnel network filter.
// This filter handles reverse tunnel connection acceptance and rejection by processing
// HTTP requests where required identification values are provided via HTTP headers.
// [#next-free-field: 6]
// [#next-free-field: 7]
message ReverseTunnel {
// Ping interval for health checks on established reverse tunnel connections.
// If not specified, defaults to ``2 seconds``.
Expand Down Expand Up @@ -113,4 +113,10 @@ message ReverseTunnel {
// ``x-envoy-reverse-tunnel-cluster-id`` headers against expected values extracted
// using format strings. Requests that fail validation are rejected with HTTP ``403 Forbidden``.
Validation validation = 5;

// Required cluster name for validating reverse tunnel connection initiations.
// When set, the filter validates that the upstream cluster of the initiator envoy matches this name
// via ``x-envoy-reverse-tunnel-upstream-cluster-name`` header. Connections with mismatched or missing
// cluster names are rejected with HTTP ``400 Bad Request``. When empty, no cluster name validation is performed.
string required_cluster_name = 6 [(validate.rules).string = {max_len: 255 ignore_empty: true}];
}
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,11 @@ new_features:
:ref:`endpoint_params <envoy_v3_api_field_extensions.http.injected_credentials.oauth2.v3.OAuth2.endpoint_params>`.
This allows passing custom parameters required by authorization servers (such as Logto or EntraID) that expect
additional body parameters during the token exchange.
- area: reverse_tunnel
change: |
Added ``required_cluster_name`` field to validate reverse tunnel initiations against the
``x-envoy-reverse-tunnel-upstream-cluster-name`` header. If initiator envoy's upstream cluster name does not match
``required_cluster_name``, connection is rejected with a ``400 Bad Request``.
- area: proto_api_scrubber
change: |
Enabled the :ref:`Proto API Scrubber <config_http_filters_proto_api_scrubber>` HTTP filter. This filter allows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ inline const Http::LowerCaseString& reverseTunnelTenantIdHeader() {
return kHeader;
}

inline const Http::LowerCaseString& reverseTunnelUpstreamClusterNameHeader() {
static const Http::LowerCaseString kHeader{
absl::StrCat(Http::Headers::get().prefix(), "-reverse-tunnel-upstream-cluster-name")};
return kHeader;
}

class ReverseConnectionMessageHandlerFactory {
public:
static std::shared_ptr<class PingMessageHandler> createPingHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ std::string RCConnectionWrapper::connect(const std::string& src_tenant_id,
::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelClusterIdHeader();
const Http::LowerCaseString& tenant_hdr =
::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelTenantIdHeader();
const Http::LowerCaseString& upstream_cluster_hdr =
::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelUpstreamClusterNameHeader();

auto headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>(
{{Http::Headers::get().Method, Http::Headers::get().MethodValues.Get},
Expand All @@ -136,6 +138,7 @@ std::string RCConnectionWrapper::connect(const std::string& src_tenant_id,
headers->addCopy(node_hdr, std::string(node_id));
headers->addCopy(cluster_hdr, std::string(cluster_id));
headers->addCopy(tenant_hdr, std::string(tenant_id));
headers->addCopy(upstream_cluster_hdr, cluster_name_);
headers->setContentLength(0);

// Encode via HTTP/1 codec.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ ReverseTunnelFilterConfig::ReverseTunnelFilterConfig(
proto_config.has_validation() &&
!proto_config.validation().dynamic_metadata_namespace().empty()
? proto_config.validation().dynamic_metadata_namespace()
: "envoy.filters.network.reverse_tunnel") {}
: "envoy.filters.network.reverse_tunnel"),
required_cluster_name_(proto_config.required_cluster_name()) {}

bool ReverseTunnelFilterConfig::validateIdentifiers(
absl::string_view node_id, absl::string_view cluster_id,
Expand Down Expand Up @@ -318,6 +319,37 @@ void ReverseTunnelFilter::RequestDecoderImpl::processIfComplete(bool end_stream)
const absl::string_view cluster_id = cluster_vals[0]->value().getStringView();
const absl::string_view tenant_id = tenant_vals[0]->value().getStringView();

// Check for upstream cluster name header and validate if required.
if (!parent_.config_->requiredClusterName().empty()) {
const auto upstream_cluster_vals = headers_->get(
Extensions::Bootstrap::ReverseConnection::reverseTunnelUpstreamClusterNameHeader());

if (upstream_cluster_vals.empty()) {
parent_.stats_.parse_error_.inc();
ENVOY_CONN_LOG(
debug, "reverse_tunnel: missing upstream cluster name header when enforcement is enabled",
parent_.read_callbacks_->connection());
sendLocalReply(Http::Code::BadRequest, "Missing upstream cluster name header", nullptr,
absl::nullopt, "reverse_tunnel_missing_cluster_name_header");
parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite);
return;
}

const absl::string_view upstream_cluster_name =
upstream_cluster_vals[0]->value().getStringView();
if (upstream_cluster_name != parent_.config_->requiredClusterName()) {
parent_.stats_.validation_failed_.inc();
ENVOY_CONN_LOG(debug,
"reverse_tunnel: upstream cluster name mismatch. Expected: '{}', Actual: '{}'",
parent_.read_callbacks_->connection(), parent_.config_->requiredClusterName(),
upstream_cluster_name);
sendLocalReply(Http::Code::BadRequest, "Cluster name mismatch", nullptr, absl::nullopt,
"reverse_tunnel_cluster_mismatch");
parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite);
return;
}
}

// Validate node_id and cluster_id if validation is configured.
auto& connection = parent_.read_callbacks_->connection();
const bool validation_passed =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class ReverseTunnelFilterConfig : public Logger::Loggable<Logger::Id::filter> {
void emitValidationMetadata(absl::string_view node_id, absl::string_view cluster_id,
bool validation_passed, StreamInfo::StreamInfo& stream_info) const;

// Returns the required cluster name for validation.
const std::string& requiredClusterName() const { return required_cluster_name_; }

private:
ReverseTunnelFilterConfig(
const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config,
Expand All @@ -67,6 +70,9 @@ class ReverseTunnelFilterConfig : public Logger::Loggable<Logger::Id::filter> {
Formatter::FormatterConstSharedPtr cluster_id_formatter_;
const bool emit_dynamic_metadata_{false};
const std::string dynamic_metadata_namespace_;

// Required cluster name for validation (empty means no validation).
const std::string required_cluster_name_;
};

using ReverseTunnelFilterConfigSharedPtr = std::shared_ptr<ReverseTunnelFilterConfig>;
Expand Down
Loading
Loading