Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ab52b39
ext_authz: implement cooperative caching bypass
toddmgreer May 5, 2026
d184a24
ext_authz: refactor http client to avoid explicit variables
toddmgreer May 6, 2026
c9b1ae7
ext_authz: apply review comments and add cache error tests
toddmgreer May 6, 2026
8fbf7ec
ext_authz: document cooperative caching bypass
toddmgreer May 6, 2026
66ee693
ext_authz: add cooperative caching release note
toddmgreer May 6, 2026
4a3daf5
Merge remote-tracking branch 'upstream/main' into ext_authz_caching
toddmgreer May 6, 2026
df2d604
ext_authz: add cache integration tests
toddmgreer May 6, 2026
1dd885d
ext_authz: refactor cache integration tests based on reviews
toddmgreer May 6, 2026
46fe75f
ext_authz: transition cache bypass to typed dynamic metadata
toddmgreer May 7, 2026
b77bfd6
ext_authz: apply code formatting and style fixes
toddmgreer May 8, 2026
b413bf0
ext_authz: update cooperative caching docs and release notes
toddmgreer May 8, 2026
dd451c9
ext_authz: support pluggable caching via TypedExtensionConfig
toddmgreer Jun 1, 2026
c74c01d
ext_authz: remove old caching bypass and fix sync lookup
toddmgreer Jun 1, 2026
65a4cfc
test(ext_authz): add unit tests for cache hit denied and error cases
toddmgreer Jun 1, 2026
b1e1934
test(ext_authz): add in-memory cache integration test
toddmgreer Jun 1, 2026
4b29cbd
Merge branch 'main' into ext_authz_caching
toddmgreer Jun 2, 2026
c4741eb
test: fix compilation error and ext_authz_test merge corruption
toddmgreer Jun 4, 2026
156fd57
Merge remote-tracking branch 'upstream/main' into ext_authz_caching
toddmgreer Jun 5, 2026
7633dd3
style: fix style and formatting in ext_authz cache tests and headers
toddmgreer Jun 10, 2026
8136cc9
ext_authz: Defer CheckRequest construction on cache hit.
toddmgreer Jun 11, 2026
35e6325
docs: Update ext_authz caching documentation.
toddmgreer Jun 11, 2026
3b71a6a
docs: Add invalid_cached_response stat back to docs.
toddmgreer Jun 11, 2026
62a45fa
ext_authz: Remove raw_check_response from Response struct.
toddmgreer Jun 11, 2026
e20c7e6
ext_authz: rename processResponse to onComplete
toddmgreer Jun 11, 2026
de5ac03
ext_authz: Remove misleading comment in collectAttributes
toddmgreer Jun 11, 2026
4646a34
style: fix formatting and proto imports in ext_authz
toddmgreer Jun 11, 2026
0125d5e
api: add extension category for ext_authz cache
toddmgreer Jun 11, 2026
6337add
Revert "api: add extension category for ext_authz cache"
toddmgreer Jun 11, 2026
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 @@ -5,6 +5,7 @@ package envoy.extensions.filters.http.ext_authz.v3;
import "envoy/config/common/mutation_rules/v3/mutation_rules.proto";
import "envoy/config/core/v3/base.proto";
import "envoy/config/core/v3/config_source.proto";
import "envoy/config/core/v3/extension.proto";
import "envoy/config/core/v3/grpc_service.proto";
import "envoy/config/core/v3/http_uri.proto";
import "envoy/type/matcher/v3/metadata.proto";
Expand All @@ -30,7 +31,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 @@ -397,6 +398,11 @@ message ExtAuthz {
//
// Defaults to ``false``.
bool shadow_mode = 32;

// Optional configuration for a cache extension. If specified, the filter will
// attempt to lookup and populate the cache for authorization requests.
// The extension must implement the "envoy.filters.http.ext_authz.cache" interface.
config.core.v3.TypedExtensionConfig cache = 33;
}

// Serialized form of the shadow-mode authorization decision written to FilterState
Expand Down
12 changes: 12 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,17 @@ 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.

ExtAuthz Caching
----------------
The External Authorization filter supports caching authorization decisions to bypass the external authorization service call.

To enable this, configure the :ref:`cache <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.cache>` field with a cache extension configuration. The extension must implement the ``envoy.filters.http.ext_authz.cache`` interface.

When caching is enabled, the filter will:
1. Perform a cache lookup using the request attributes.
2. If a cache hit occurs, the filter will bypass the external service call and apply the cached response (OK with mutations, Denied, or Error) directly.
3. If a cache miss occurs, the filter will proceed with the live call to the external authorization service and, upon receiving a response, will populate the cache with the response.

Statistics
----------
.. _config_http_filters_ext_authz_stats:
Expand All @@ -199,6 +210,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
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) {
EMPTY_STRING,
Http::Code::OK,
Protobuf::Struct{}}};

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

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

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

Expand Down
15 changes: 15 additions & 0 deletions source/extensions/filters/http/ext_authz/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "auth_cache_interface",
hdrs = ["auth_cache.h"],
deps = [
"//envoy/config:typed_config_interface",
"//envoy/http:header_map_interface",
"//envoy/stream_info:stream_info_interface",
"//envoy/tracing:tracer_interface",
"//source/extensions/filters/common/ext_authz:ext_authz_interface",
"@envoy_api//envoy/service/auth/v3:pkg_cc_proto",
],
)

envoy_cc_library(
name = "ext_authz",
srcs = ["ext_authz.cc"],
hdrs = ["ext_authz.h"],
deps = [
":auth_cache_interface",
"//envoy/http:codes_interface",
"//envoy/stats:stats_macros",
"//source/common/buffer:buffer_lib",
Expand Down Expand Up @@ -44,6 +58,7 @@ envoy_cc_extension(
srcs = ["config.cc"],
hdrs = ["config.h"],
deps = [
":auth_cache_interface",
":ext_authz",
"//envoy/registry",
"//envoy/stats:stats_macros",
Expand Down
69 changes: 69 additions & 0 deletions source/extensions/filters/http/ext_authz/auth_cache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once

#include <memory>

#include "envoy/config/typed_config.h"
#include "envoy/http/filter.h"
#include "envoy/http/header_map.h"
#include "envoy/service/auth/v3/external_auth.pb.h"
#include "envoy/stream_info/stream_info.h"
#include "envoy/tracing/tracer.h"

#include "source/common/protobuf/protobuf.h"
#include "source/extensions/filters/common/ext_authz/ext_authz.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace ExtAuthz {

struct RequestAttributes {
const Http::RequestHeaderMap& headers_;
Protobuf::Map<std::string, std::string> context_extensions_;
envoy::config::core::v3::Metadata metadata_context_;
envoy::config::core::v3::Metadata route_metadata_context_;
};

class AuthCache {
public:
virtual ~AuthCache() = default;

using LookupCallback = std::function<void(Filters::Common::ExtAuthz::ResponsePtr&&)>;

/**
* Looks for a matching request/response pair in the cache.
* If lookup fails or misses, the callback should be invoked with nullptr.
* Lifetimes of the arguments passed to it must last until onDestroy is called.
* @param decoder_callbacks The stream decoder filter callbacks.
* @param attributes The RequestAttributes containing authorization context.
* @param cb The callback to invoke when the lookup completes.
*/
virtual void lookup(Http::StreamDecoderFilterCallbacks& decoder_callbacks,
const RequestAttributes& attributes, LookupCallback&& cb) = 0;

/**
* Inserts a response into the cache.
* @param response The Response received from the authz service.
*/
virtual void insert(const Filters::Common::ExtAuthz::Response& response) = 0;

/**
* Called when the filter is being destroyed. The cache implementation must
* abort any in-progress asynchronous operations before returning.
*/
virtual void onDestroy() = 0;
};

using AuthCachePtr = std::unique_ptr<AuthCache>;

class AuthCacheFactory : public Config::TypedFactory {
public:
virtual AuthCachePtr createAuthCache(const Protobuf::Message& config,
Server::Configuration::ServerFactoryContext& context) = 0;
std::string category() const override { return "envoy.filters.http.ext_authz.cache"; }
};

} // namespace ExtAuthz
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
36 changes: 29 additions & 7 deletions source/extensions/filters/http/ext_authz/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "source/common/protobuf/utility.h"
#include "source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.h"
#include "source/extensions/filters/common/ext_authz/ext_authz_http_impl.h"
#include "source/extensions/filters/http/ext_authz/auth_cache.h"
#include "source/extensions/filters/http/ext_authz/ext_authz.h"

namespace Envoy {
Expand All @@ -25,6 +26,18 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ
const std::string& stats_prefix, Server::Configuration::ServerFactoryContext& server_context) {
const auto filter_config = std::make_shared<FilterConfig>(proto_config, server_context.scope(),
stats_prefix, server_context);

AuthCacheFactory* const cache_factory =
proto_config.has_cache()
? &Config::Utility::getAndCheckFactory<AuthCacheFactory>(proto_config.cache())
: nullptr;
const std::shared_ptr<Protobuf::Message> shared_cache_config =
cache_factory != nullptr
? Config::Utility::translateAnyToFactoryConfig(proto_config.cache().typed_config(),
server_context.messageValidationVisitor(),
*cache_factory)
: nullptr;

// The callback is created in main thread and executed in worker thread, variables except factory
// context must be captured by value into the callback.
Http::FilterFactoryCb callback;
Expand All @@ -36,12 +49,16 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ
const auto client_config =
std::make_shared<Extensions::Filters::Common::ExtAuthz::ClientConfig>(
proto_config, timeout_ms, proto_config.http_service().path_prefix(), server_context);
callback = [filter_config = std::move(filter_config), client_config,
&server_context](Http::FilterChainFactoryCallbacks& callbacks) {
callback = [filter_config = std::move(filter_config), client_config, &server_context,
cache_factory, shared_cache_config](Http::FilterChainFactoryCallbacks& callbacks) {
auto client = std::make_unique<Extensions::Filters::Common::ExtAuthz::RawHttpClientImpl>(
server_context.clusterManager(), client_config);
callbacks.addStreamFilter(
std::make_shared<Filter>(filter_config, std::move(client), server_context));
AuthCachePtr cache =
cache_factory != nullptr
? cache_factory->createAuthCache(*shared_cache_config, server_context)
: nullptr;
callbacks.addStreamFilter(std::make_shared<Filter>(filter_config, std::move(client),
server_context, std::move(cache)));
};
} else {
// gRPC client.
Expand All @@ -56,16 +73,21 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ
Envoy::Grpc::GrpcServiceConfigWithHashKey config_with_hash_key =
Envoy::Grpc::GrpcServiceConfigWithHashKey(proto_config.grpc_service());
callback = [&server_context, filter_config = std::move(filter_config), timeout,
config_with_hash_key](Http::FilterChainFactoryCallbacks& callbacks) {
config_with_hash_key, cache_factory,
shared_cache_config](Http::FilterChainFactoryCallbacks& callbacks) {
auto client_or_error = server_context.clusterManager()
.grpcAsyncClientManager()
.getOrCreateRawAsyncClientWithHashKey(
config_with_hash_key, server_context.scope(), true);
THROW_IF_NOT_OK_REF(client_or_error.status());
auto client = std::make_unique<Filters::Common::ExtAuthz::GrpcClientImpl>(
client_or_error.value(), timeout);
callbacks.addStreamFilter(
std::make_shared<Filter>(filter_config, std::move(client), server_context));
AuthCachePtr cache =
cache_factory != nullptr
? cache_factory->createAuthCache(*shared_cache_config, server_context)
: nullptr;
callbacks.addStreamFilter(std::make_shared<Filter>(filter_config, std::move(client),
server_context, std::move(cache)));
};
}
return callback;
Expand Down
Loading
Loading