From 1de17b0b24be7bdfd6df6ea7ec660781436e1e06 Mon Sep 17 00:00:00 2001 From: "update-envoy[bot]" <135279899+update-envoy[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 03:12:26 +0000 Subject: [PATCH] redis_proxy: add support for external authentication (#35643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolves #35673 ## PR overview Redis proxy users may want to create advanced authentication methods. For example, the official [Azure SDK extension for Redis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis) allows to authenticate to a Redis server using Microsoft Entra ID token-based authentication, by passing a token in the password argument of the `AUTH` command periodically, based on token expiration. This PR introduces a way to support external authentication via a gRPC service with additional support for expiry of such authentication (e.g. for token-based authentication). This way we keep it extensible for **any** advanced authentication methods users might want to develop. ### The reviewer may ask: Why not use the _ext_authz_ filter? The cost/latency impact by using the _ext_authz_ filter is much bigger than the proposed design. That's because instead of being called on every request, the current design only calls the external dependency on **AUTH** commands. Not only that, but also we would have to decode the Redis protocol twice, if we used a separate filter. --- Risk Level: Medium (small optional feature added to existing filter) Testing: ✅ - Unit Tests - Integration Tests - Manual Testing ![image](https://github.com/user-attachments/assets/3caab358-7c37-446d-8e12-bff9c1442948) - Also, we are already using the signed _-dev_ build on a test AKS cluster Docs Changes: ✅ - Proto docs ![image](https://github.com/user-attachments/assets/1432114f-ff93-431a-90ad-1c1262989e8c) - Updated authentication-related information on the Redis protocol page. Release Notes: ✅ --------- Signed-off-by: Diogo Barbosa Signed-off-by: Diogo Barbosa Mirrored from https://github.com/envoyproxy/envoy @ 67b69c9038402b88953a2ab171ae38cab5cb23ab --- BUILD | 1 + .../network/redis_proxy/v3/redis_proxy.proto | 31 +++++++++++- envoy/service/redis_auth/v3/BUILD | 10 ++++ .../redis_auth/v3/redis_external_auth.proto | 47 +++++++++++++++++++ versioning/BUILD | 1 + 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 envoy/service/redis_auth/v3/BUILD create mode 100644 envoy/service/redis_auth/v3/redis_external_auth.proto diff --git a/BUILD b/BUILD index 931fbea9..cae22a8d 100644 --- a/BUILD +++ b/BUILD @@ -360,6 +360,7 @@ proto_library( "//envoy/service/metrics/v3:pkg", "//envoy/service/rate_limit_quota/v3:pkg", "//envoy/service/ratelimit/v3:pkg", + "//envoy/service/redis_auth/v3:pkg", "//envoy/service/route/v3:pkg", "//envoy/service/runtime/v3:pkg", "//envoy/service/secret/v3:pkg", diff --git a/envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.proto b/envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.proto index 28e351fb..19ab3b45 100644 --- a/envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.proto +++ b/envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.network.redis_proxy.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/grpc_service.proto"; import "envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto"; import "google/protobuf/duration.proto"; @@ -25,7 +26,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Redis Proxy :ref:`configuration overview `. // [#extension: envoy.filters.network.redis_proxy] -// [#next-free-field: 10] +// [#next-free-field: 11] message RedisProxy { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.redis_proxy.v2.RedisProxy"; @@ -353,6 +354,15 @@ message RedisProxy { // client. If an AUTH command is received when the password is not set, then an "ERR Client sent // AUTH, but no ACL is set" error will be returned. config.core.v3.DataSource downstream_auth_username = 7 [(udpa.annotations.sensitive) = true]; + + // External authentication configuration. If set, instead of validating username and password against ``downstream_auth_username`` and ``downstream_auth_password``, + // the filter will call an external gRPC service to authenticate the client. + // A typical usage of this feature is for situations where the password is a one-time token that needs to be validated against a remote service, like a sidecar. + // Expiration is also supported, which will disable any further commands from the client after the expiration time, unless a new AUTH command is received and the external auth service returns a new expiration time. + // If the external auth service returns an error, authentication is considered failed. + // If this setting is set together with ``downstream_auth_username`` and ``downstream_auth_password``, the external auth service will be source of truth, but those fields will still be used for downstream authentication to the cluster. + // The API is defined by :ref:`RedisProxyExternalAuthRequest `. + RedisExternalAuthProvider external_auth_provider = 10; } // RedisProtocolOptions specifies Redis upstream protocol options. This object is used in @@ -370,3 +380,22 @@ message RedisProtocolOptions { // ``_ in the server's configuration file. config.core.v3.DataSource auth_username = 2 [(udpa.annotations.sensitive) = true]; } + +// RedisExternalAuthProvider specifies a gRPC service that can be used to authenticate Redis clients. +// This service will be called every time an AUTH command is received from a client. +// If the service returns an error, authentication is considered failed. +// If the service returns a success, the client is considered authenticated. +// The service can also return an expiration timestamp, which will be used to disable any further +// commands from the client after it passes, unless a new AUTH command is received and the +// external auth service returns a new expiration timestamp. +message RedisExternalAuthProvider { + // External auth gRPC service configuration. + // It will be called every time an AUTH command is received from a client. + config.core.v3.GrpcService grpc_service = 1; + + // If set to true, the filter will expect an expiration timestamp in the response from the external + // auth service. This timestamp will be used to disable any further commands from the client after + // the expiration time, unless a new AUTH command is received and the external auth service returns + // a new expiration timestamp. + bool enable_auth_expiration = 2; +} diff --git a/envoy/service/redis_auth/v3/BUILD b/envoy/service/redis_auth/v3/BUILD new file mode 100644 index 00000000..1e4f23a1 --- /dev/null +++ b/envoy/service/redis_auth/v3/BUILD @@ -0,0 +1,10 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + has_services = True, + deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], +) diff --git a/envoy/service/redis_auth/v3/redis_external_auth.proto b/envoy/service/redis_auth/v3/redis_external_auth.proto new file mode 100644 index 00000000..52bc3bc0 --- /dev/null +++ b/envoy/service/redis_auth/v3/redis_external_auth.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package envoy.service.redis_auth.v3; + +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.service.redis_auth.v3"; +option java_outer_classname = "RedisExternalAuthProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/redis_auth/v3;redis_authv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Redis external authentication service] + +// The messages used by the redis_proxy filter when performing external authentication. + +// A generic interface for performing external password authentication on incoming AUTH commands. +service RedisProxyExternalAuth { + // Performs authentication check based on the data sent with the AUTH request. + // Returns either an OK status or an error status. + rpc Authenticate(RedisProxyExternalAuthRequest) returns (RedisProxyExternalAuthResponse) { + } +} + +message RedisProxyExternalAuthRequest { + // Username, if applicable. Otherwise, empty. + string username = 1; + + // Password sent with the AUTH command. + string password = 2; +} + +message RedisProxyExternalAuthResponse { + // Status of the authentication check. + google.rpc.Status status = 1; + + // Optional expiration time for the authentication. + // If set, the authentication will be valid until this time. + // If not set, the authentication will be valid indefinitely. + google.protobuf.Timestamp expiration = 2; + + // Optional message to be sent back to the client. + string message = 3; +} diff --git a/versioning/BUILD b/versioning/BUILD index ea5e0db9..bb27ffae 100644 --- a/versioning/BUILD +++ b/versioning/BUILD @@ -299,6 +299,7 @@ proto_library( "//envoy/service/metrics/v3:pkg", "//envoy/service/rate_limit_quota/v3:pkg", "//envoy/service/ratelimit/v3:pkg", + "//envoy/service/redis_auth/v3:pkg", "//envoy/service/route/v3:pkg", "//envoy/service/runtime/v3:pkg", "//envoy/service/secret/v3:pkg",