Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/api/bazel-*
/bazel-*
/ci/bazel-*
/docs/bazel-*
/mobile/bazel-*
bazel.output.txt
clang.bazelrc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// External Authorization :ref:`configuration overview <config_http_filters_ext_authz>`.
// [#extension: envoy.filters.http.ext_authz]

// [#next-free-field: 33]
// [#next-free-field: 34]
message ExtAuthz {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.ext_authz.v3.ExtAuthz";
Expand Down Expand Up @@ -386,6 +386,11 @@ message ExtAuthz {
//
// Defaults to ``false``.
bool shadow_mode = 32;

// If set, enables bypassing the external authorization service call by retrieving
// a cached CheckResponse directly from dynamic typed metadata under this namespace.
// If this namespace is empty, the cooperative caching bypass feature is disabled.
string check_response_typed_metadata_namespace = 33;
}

// Serialized form of the shadow-mode authorization decision written to FilterState
Expand Down
9 changes: 9 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ new_features:
RSA public key exchange on behalf of the client. Added a new
:ref:`downstream_ssl <envoy_v3_api_field_extensions.filters.network.mysql_proxy.v3.MySQLProxy.downstream_ssl>`
config option with ``DISABLE``, ``REQUIRE``, and ``ALLOW`` modes.
- area: ext_authz
change: |
Added strongly-typed cooperative caching bypass support to the external authorization HTTP filter. The filter can
now retrieve a cached ``CheckResponse`` directly from dynamic typed metadata, unpack it from ``google.protobuf.Any``,
and apply the cached response directly, bypassing the live authorization service call. If unpacking fails (due to type
mismatch), the filter increments the new ``invalid_cached_response`` statistic and gracefully falls back to a live
network call. Configured via the new
:ref:`check_response_typed_metadata_namespace <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.check_response_typed_metadata_namespace>`
option.
- area: quic
change: |
Added support for TLS session ticket resumption in QUIC using configured session ticket keys from
Expand Down
15 changes: 15 additions & 0 deletions docs/root/configuration/http/http_filters/ext_authz_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,20 @@ In this configuration:
This pattern provides clean separation between the decision logic (in the Lua filter) and the authorization
enforcement (in ext_authz), while ensuring the ext_authz filter is only instantiated and invoked when needed.

Cooperative Caching Bypass
--------------------------
The External Authorization filter supports bypassing the external authorization service call by retrieving a cached response from dynamic typed metadata. This is designed to work cooperatively with a preceding caching filter (such as a suitably configured ext_proc filter using an external cache).

To enable this, configure the :ref:`check_response_typed_metadata_namespace <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.check_response_typed_metadata_namespace>` with the dynamic typed metadata namespace where the cached response is stored.

When a cached response is present under the configured namespace:
1. The filter will retrieve the ``google.protobuf.Any`` message directly from the dynamic typed metadata.
2. It will attempt to unpack it directly into a ``CheckResponse`` proto.
3. If unpacking succeeds, the filter will bypass the external service call and apply the cached response (OK with mutations, Denied, or Error) directly.
4. If unpacking fails (due to type mismatch), the filter will increment the ``invalid_cached_response`` stat and gracefully fall back to making a live call to the external authorization service.

On cache misses (when no cached response is found under the namespace), the filter will proceed with the live call and, upon receiving a response, will pack the raw ``CheckResponse`` proto directly into a ``google.protobuf.Any`` message and write it to the dynamic typed metadata under the configured namespace, allowing the caching filter to record and cache it in raw binary format.

Statistics
----------
.. _config_http_filters_ext_authz_stats:
Expand All @@ -199,6 +213,7 @@ The HTTP filter outputs statistics in the ``cluster.<route target cluster>.ext_a
because it couldn't apply all header mutations"
response_header_limits_reached, Counter, "Total responses for which ext_authz sent a local reply
because it couldn't apply all header mutations"
invalid_cached_response, Counter, Total cached responses that failed to be Base64-decoded or parsed.

Dynamic Metadata
----------------
Expand Down
2 changes: 2 additions & 0 deletions source/extensions/filters/common/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ struct Response {
// The gRPC status returned by the authorization server when it is making a
// gRPC call.
absl::optional<Grpc::Status::GrpcStatus> grpc_status{absl::nullopt};
// The raw CheckResponse proto, if available.
absl::optional<envoy::service::auth::v3::CheckResponse> raw_check_response;
};

using ResponsePtr = std::unique_ptr<Response>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ void GrpcClientImpl::onSuccess(std::unique_ptr<envoy::service::auth::v3::CheckRe
ENVOY_LOG(trace, "Received CheckResponse: {}", response->DebugString());
ResponsePtr authz_response = std::make_unique<Response>(Response{});
authz_response->grpc_status = response->status().code();
authz_response->raw_check_response = *response;
if (response->status().code() == Grpc::Status::WellKnownGrpcStatus::Ok) {
span.setTag(TracingConstants::get().TraceStatus, TracingConstants::get().TraceOk);
authz_response->status = CheckStatus::OK;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,13 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) {
// codes. A Forbidden response is sent to the client if the filter has not been configured with
// failure_mode_allow.
if (Http::CodeUtility::is5xx(status_code)) {
return std::make_unique<Response>(errorResponse());
auto response = std::make_unique<Response>(errorResponse());
response->raw_check_response.emplace();
response->raw_check_response->mutable_status()->set_code(
Grpc::Status::WellKnownGrpcStatus::Internal);
response->raw_check_response->mutable_error_response()->mutable_status()->set_code(
static_cast<envoy::type::v3::StatusCode>(status_code));
return response;
}

// Extract headers-to-remove from the storage header coming from the
Expand Down Expand Up @@ -477,6 +483,37 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) {
EMPTY_STRING,
Http::Code::OK,
Protobuf::Struct{}}};

ok.response_->raw_check_response.emplace();
ok.response_->raw_check_response->mutable_status()->set_code(
Grpc::Status::WellKnownGrpcStatus::Ok);
auto* ok_response = ok.response_->raw_check_response->mutable_ok_response();

for (const auto& header : ok.response_->headers_to_set) {
auto* h = ok_response->add_headers();
h->mutable_header()->set_key(header.first);
h->mutable_header()->set_value(header.second);
h->mutable_append()->set_value(false);
}
for (const auto& header : ok.response_->headers_to_add) {
auto* h = ok_response->add_headers();
h->mutable_header()->set_key(header.first);
h->mutable_header()->set_value(header.second);
h->mutable_append()->set_value(true);
}
for (const auto& header : ok.response_->response_headers_to_add) {
auto* h = ok_response->add_response_headers_to_add();
h->mutable_header()->set_key(header.first);
h->mutable_header()->set_value(header.second);
h->set_append_action(Router::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD);
}
for (const auto& header : ok.response_->headers_to_remove) {
ok_response->add_headers_to_remove(header);
}
if (!ok.response_->dynamic_metadata.fields().empty()) {
*ok_response->mutable_dynamic_metadata() = ok.response_->dynamic_metadata;
}

return std::move(ok.response_);
}

Expand Down Expand Up @@ -504,6 +541,36 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) {
message->bodyAsString(),
static_cast<Http::Code>(status_code),
Protobuf::Struct{}}};

denied.response_->raw_check_response.emplace();
const auto grpc_status = (status_code == enumToInt(Http::Code::Unauthorized))
? Grpc::Status::WellKnownGrpcStatus::Unauthenticated
: Grpc::Status::WellKnownGrpcStatus::PermissionDenied;
denied.response_->raw_check_response->mutable_status()->set_code(grpc_status);

auto* denied_response = denied.response_->raw_check_response->mutable_denied_response();
denied_response->mutable_status()->set_code(
static_cast<envoy::type::v3::StatusCode>(status_code));
denied_response->set_body(denied.response_->body);

for (const auto& header : denied.response_->headers_to_set) {
auto* h = denied_response->add_headers();
h->mutable_header()->set_key(header.first);
h->mutable_header()->set_value(header.second);
h->mutable_append()->set_value(false);
}
for (const auto& header : denied.response_->headers_to_append) {
auto* h = denied_response->add_headers();
h->mutable_header()->set_key(header.first);
h->mutable_header()->set_value(header.second);
h->mutable_append()->set_value(true);
}

if (!denied.response_->dynamic_metadata.fields().empty()) {
*denied.response_->raw_check_response->mutable_dynamic_metadata() =
denied.response_->dynamic_metadata;
}

return std::move(denied.response_);
}

Expand Down
162 changes: 160 additions & 2 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include "source/common/router/config_impl.h"
#include "source/extensions/filters/common/processing_effect/processing_effect.h"

#include "absl/strings/escaping.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
Expand Down Expand Up @@ -75,6 +77,68 @@ bool headersWithinLimits(const Http::HeaderMap& headers) {
headers.byteSize() <= headers.maxHeadersKb() * 1024;
}

void copyHeaderFieldIntoResponse(
Filters::Common::ExtAuthz::ResponsePtr& response,
const Protobuf::RepeatedPtrField<envoy::config::core::v3::HeaderValueOption>& headers) {
for (const auto& header : headers) {
if (header.append().value()) {
response->headers_to_append.emplace_back(header.header().key(), header.header().value());
} else {
response->headers_to_set.emplace_back(header.header().key(), header.header().value());
}
}
}

void copyOkResponseMutations(Filters::Common::ExtAuthz::ResponsePtr& response,
const envoy::service::auth::v3::OkHttpResponse& ok_response) {
copyHeaderFieldIntoResponse(response, ok_response.headers());

for (const auto& header : ok_response.response_headers_to_add()) {
if (header.has_append()) {
if (header.append().value()) {
response->response_headers_to_add.emplace_back(header.header().key(),
header.header().value());
} else {
response->response_headers_to_set.emplace_back(header.header().key(),
header.header().value());
}
} else {
switch (header.append_action()) {
case Router::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD:
response->response_headers_to_add.emplace_back(header.header().key(),
header.header().value());
break;
case Router::HeaderValueOption::ADD_IF_ABSENT:
response->response_headers_to_add_if_absent.emplace_back(header.header().key(),
header.header().value());
break;
case Router::HeaderValueOption::OVERWRITE_IF_EXISTS:
response->response_headers_to_overwrite_if_exists.emplace_back(header.header().key(),
header.header().value());
break;
case Router::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD:
response->response_headers_to_set.emplace_back(header.header().key(),
header.header().value());
break;
default:
response->saw_invalid_append_actions = true;
break;
}
}
}

response->headers_to_remove = std::vector<std::string>{ok_response.headers_to_remove().begin(),
ok_response.headers_to_remove().end()};

for (const auto& query_parameter : ok_response.query_parameters_to_set()) {
response->query_parameters_to_set.emplace_back(query_parameter.key(), query_parameter.value());
}

response->query_parameters_to_remove =
std::vector<std::string>{ok_response.query_parameters_to_remove().begin(),
ok_response.query_parameters_to_remove().end()};
}

} // namespace

FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3::ExtAuthz& config,
Expand Down Expand Up @@ -136,6 +200,7 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3
include_tls_session_(config.include_tls_session()),
charge_cluster_response_stats_(
PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, charge_cluster_response_stats, true)),
check_response_typed_metadata_namespace_(config.check_response_typed_metadata_namespace()),
stats_(generateStats(stats_prefix, config.stat_prefix(), scope)),
ext_authz_ok_(pool_.add(createPoolStatName(config.stat_prefix(), "ok"))),
ext_authz_denied_(pool_.add(createPoolStatName(config.stat_prefix(), "denied"))),
Expand Down Expand Up @@ -397,6 +462,78 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
initiating_call_ = false;
}

bool Filter::tryCacheHit() {
const std::string& metadata_namespace = config_->checkResponseTypedMetadataNamespace();
if (metadata_namespace.empty()) {
return false;
}

const auto& typed_metadata =
decoder_callbacks_->streamInfo().dynamicMetadata().typed_filter_metadata();
const auto cache_it = typed_metadata.find(metadata_namespace);
if (cache_it == typed_metadata.end()) {
return false;
}

envoy::service::auth::v3::CheckResponse check_response;
auto status = MessageUtil::unpackTo(cache_it->second, check_response);
if (!status.ok()) {
ENVOY_STREAM_LOG(warn, "ext_authz failed to unpack cached CheckResponse in namespace {}",
*decoder_callbacks_, metadata_namespace);
stats_.invalid_cached_response_.inc();
return false;
}

ENVOY_STREAM_LOG(debug, "ext_authz found cached CheckResponse in typed namespace {}",
*decoder_callbacks_, metadata_namespace);

Filters::Common::ExtAuthz::ResponsePtr authz_response =
std::make_unique<Filters::Common::ExtAuthz::Response>();

authz_response->grpc_status = check_response.status().code();
authz_response->raw_check_response = check_response;
if (check_response.status().code() == Grpc::Status::WellKnownGrpcStatus::Ok) {
authz_response->status = Filters::Common::ExtAuthz::CheckStatus::OK;
if (check_response.has_ok_response()) {
copyOkResponseMutations(authz_response, check_response.ok_response());
}
} else if (check_response.has_error_response()) {
authz_response->status = Filters::Common::ExtAuthz::CheckStatus::Error;
const auto& error_response = check_response.error_response();
copyHeaderFieldIntoResponse(authz_response, error_response.headers());
const uint32_t status_code = error_response.status().code();
if (status_code > 0) {
authz_response->status_code = static_cast<Http::Code>(status_code);
}
authz_response->body = error_response.body();
} else {
ASSERT(check_response.has_denied_response());
authz_response->status = Filters::Common::ExtAuthz::CheckStatus::Denied;
authz_response->status_code = Http::Code::Forbidden;
if (check_response.has_denied_response()) {
copyHeaderFieldIntoResponse(authz_response, check_response.denied_response().headers());
const uint32_t status_code = check_response.denied_response().status().code();
if (status_code > 0) {
authz_response->status_code = static_cast<Http::Code>(status_code);
}
authz_response->body = check_response.denied_response().body();
}
}

if (check_response.has_ok_response() && check_response.ok_response().has_dynamic_metadata()) {
authz_response->dynamic_metadata = check_response.ok_response().dynamic_metadata();
} else {
authz_response->dynamic_metadata = check_response.dynamic_metadata();
}

initiating_call_ = true;
filter_return_ = FilterReturn::StopDecoding;
applyResponse(std::move(authz_response));
initiating_call_ = false;

return true;
}

Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) {
const auto per_route_flags = getPerRouteFlags(decoder_callbacks_->route());
skip_check_ = per_route_flags.skip_check_;
Expand Down Expand Up @@ -435,6 +572,13 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
}

request_headers_ = &headers;

if (tryCacheHit()) {
return filter_return_ == FilterReturn::StopDecoding
? Http::FilterHeadersStatus::StopAllIterationAndWatermark
: Http::FilterHeadersStatus::Continue;
}

const auto& check_settings = per_route_flags.check_settings_;
buffer_data_ = (config_->withRequestBody() || check_settings.has_with_request_body()) &&
!check_settings.disable_request_body_buffering() &&
Expand Down Expand Up @@ -651,11 +795,25 @@ CheckResult Filter::validateAndCheckDecoderHeaderMutation(

void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
state_ = State::Complete;
using Filters::Common::ExtAuthz::CheckStatus;
Stats::StatName empty_stat_name;

updateLoggingInfo(response->grpc_status);

const std::string& metadata_namespace = config_->checkResponseTypedMetadataNamespace();
if (!metadata_namespace.empty() && response->raw_check_response.has_value()) {
Protobuf::Any typed_metadata;
typed_metadata.PackFrom(response->raw_check_response.value());
decoder_callbacks_->streamInfo().setDynamicTypedMetadata(metadata_namespace, typed_metadata);
ENVOY_STREAM_LOG(debug, "ext_authz stored CheckResponse in typed namespace {}",
*decoder_callbacks_, metadata_namespace);
}

applyResponse(std::move(response));
}

void Filter::applyResponse(Filters::Common::ExtAuthz::ResponsePtr response) {
using Filters::Common::ExtAuthz::CheckStatus;
Stats::StatName empty_stat_name;

if (response->saw_invalid_append_actions) {
if (config_->validateMutations()) {
ENVOY_STREAM_LOG(trace, "Rejecting response with invalid header append action.",
Expand Down
Loading
Loading