Skip to content
Closed
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 @@ -22,7 +22,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// Redis Proxy :ref:`configuration overview <config_network_filters_redis_proxy>`.
// [#extension: envoy.filters.network.redis_proxy]

// [#next-free-field: 7]
// [#next-free-field: 8]
message RedisProxy {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.network.redis_proxy.v2.RedisProxy";
Expand Down Expand Up @@ -234,6 +234,18 @@ message RedisProxy {
// client. If an AUTH command is received when the password is not set, then an "ERR Client sent
// AUTH, but no password is set" error will be returned.
config.core.v3.DataSource downstream_auth_password = 6 [(udpa.annotations.sensitive) = true];

// If a username is provided an ACL style AUTH command will be required with a username and password.
// Authenticate Redis client connections locally by forcing downstream clients to issue a `Redis
// AUTH command <https://redis.io/commands/auth>`_ with this username and the *downstream_auth_password*
// before enabling any other command. If an AUTH command's username and password matches this username
// and the *downstream_auth_password* , an "OK" response will be returned to the client. If the AUTH
// command username or password does not match this username or the *downstream_auth_password*, then an
// "WRONGPASS invalid username-password pair" error will be returned. If any other command is received before AUTH when this
// password is set, then a "NOAUTH Authentication required." error response will be sent to the
// 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];
}

// RedisProtocolOptions specifies Redis upstream protocol options. This object is used in
Expand All @@ -246,4 +258,8 @@ message RedisProtocolOptions {
// Upstream server password as defined by the `requirepass` directive
// <https://redis.io/topics/config>`_ in the server's configuration file.
config.core.v3.DataSource auth_password = 1 [(udpa.annotations.sensitive) = true];

// Upstream server username as defined by the `user` directive
// <https://redis.io/topics/acl>`_ in the server's configuration file.
config.core.v3.DataSource auth_username = 2 [(udpa.annotations.sensitive) = true];
}
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ New Features
* network filters: added a :ref:`postgres proxy filter <config_network_filters_postgres_proxy>`.
* network filters: added a :ref:`rocketmq proxy filter <config_network_filters_rocketmq_proxy>`.
* ratelimit: added :ref:`API version <envoy_v3_api_field_config.ratelimit.v3.RateLimitServiceConfig.transport_api_version>` to explicitly set the version of gRPC service endpoint and message to be used.
* redis: added acl support :ref:`downstream_auth_username <envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProxy.downstream_auth_username>` for downstream client ACL authentication, and :ref:`auth_username <envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProtocolOptions.auth_username>` to configure authentication usernames for upstream Redis 6+ server clusters with ACL enabled.
* request_id: added to :ref:`always_set_request_id_in_response setting <envoy_v3_api_field_extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.always_set_request_id_in_response>`
to set :ref:`x-request-id <config_http_conn_man_headers_x-request-id>` header in response even if
tracing is not forced.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion source/extensions/clusters/redis/redis_cluster.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ RedisCluster::RedisCluster(
: Config::Utility::translateClusterHosts(cluster.hidden_envoy_deprecated_hosts())),
local_info_(factory_context.localInfo()), random_(factory_context.random()),
redis_discovery_session_(*this, redis_client_factory), lb_factory_(std::move(lb_factory)),
auth_username_(
NetworkFilters::RedisProxy::ProtocolOptionsConfigImpl::authUsername(info(), api)),
auth_password_(
NetworkFilters::RedisProxy::ProtocolOptionsConfigImpl::authPassword(info(), api)),
cluster_name_(cluster.name()),
Expand Down Expand Up @@ -278,7 +280,8 @@ void RedisCluster::RedisDiscoverySession::startResolveRedis() {
client = std::make_unique<RedisDiscoveryClient>(*this);
client->host_ = current_host_address_;
client->client_ = client_factory_.create(host, dispatcher_, *this, redis_command_stats_,
parent_.info()->statsScope(), parent_.auth_password_);
parent_.info()->statsScope(), parent_.auth_username_,
parent_.auth_password_);
client->client_->addConnectionCallbacks(*client);
}

Expand Down
1 change: 1 addition & 0 deletions source/extensions/clusters/redis/redis_cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ class RedisCluster : public Upstream::BaseDynamicClusterImpl {
Upstream::HostVector hosts_;
Upstream::HostMap all_hosts_;

const std::string auth_username_;
const std::string auth_password_;
const std::string cluster_name_;
const Common::Redis::ClusterRefreshManagerSharedPtr refresh_manager_;
Expand Down
5 changes: 3 additions & 2 deletions source/extensions/filters/network/common/redis/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class Client : public Event::DeferredDeletable {
* Initialize the connection. Issue the auth command and readonly command as needed.
* @param auth password for upstream host.
*/
virtual void initialize(const std::string& auth_password) PURE;
virtual void initialize(const std::string& auth_username, const std::string& auth_password) PURE;
};

using ClientPtr = std::unique_ptr<Client>;
Expand Down Expand Up @@ -206,7 +206,8 @@ class ClientFactory {
virtual ClientPtr create(Upstream::HostConstSharedPtr host, Event::Dispatcher& dispatcher,
const Config& config,
const RedisCommandStatsSharedPtr& redis_command_stats,
Stats::Scope& scope, const std::string& auth_password) PURE;
Stats::Scope& scope, const std::string& auth_username,
const std::string& auth_password) PURE;
};

} // namespace Client
Expand Down
13 changes: 9 additions & 4 deletions source/extensions/filters/network/common/redis/client_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,12 @@ void ClientImpl::PendingRequest::cancel() {
canceled_ = true;
}

void ClientImpl::initialize(const std::string& auth_password) {
if (!auth_password.empty()) {
void ClientImpl::initialize(const std::string& auth_username, const std::string& auth_password) {
if (!auth_username.empty()) {
// Send an AUTH command to the upstream server with username and password.
Utility::AuthRequest auth_request(auth_username, auth_password);
makeRequest(auth_request, null_pool_callbacks);
} else if (!auth_password.empty()) {
// Send an AUTH command to the upstream server.
Utility::AuthRequest auth_request(auth_password);
makeRequest(auth_request, null_pool_callbacks);
Expand All @@ -304,10 +308,11 @@ ClientFactoryImpl ClientFactoryImpl::instance_;
ClientPtr ClientFactoryImpl::create(Upstream::HostConstSharedPtr host,
Event::Dispatcher& dispatcher, const Config& config,
const RedisCommandStatsSharedPtr& redis_command_stats,
Stats::Scope& scope, const std::string& auth_password) {
Stats::Scope& scope, const std::string& auth_username,
const std::string& auth_password) {
ClientPtr client = ClientImpl::create(host, dispatcher, EncoderPtr{new EncoderImpl()},
decoder_factory_, config, redis_command_stats, scope);
client->initialize(auth_password);
client->initialize(auth_username, auth_password);
return client;
}

Expand Down
5 changes: 3 additions & 2 deletions source/extensions/filters/network/common/redis/client_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ClientImpl : public Client, public DecoderCallbacks, public Network::Conne
PoolRequest* makeRequest(const RespValue& request, ClientCallbacks& callbacks) override;
bool active() override { return !pending_requests_.empty(); }
void flushBufferAndResetTimer();
void initialize(const std::string& auth_password) override;
void initialize(const std::string& auth_username, const std::string& auth_password) override;

private:
friend class RedisClientImplTest;
Expand Down Expand Up @@ -151,7 +151,8 @@ class ClientFactoryImpl : public ClientFactory {
// RedisProxy::ConnPool::ClientFactoryImpl
ClientPtr create(Upstream::HostConstSharedPtr host, Event::Dispatcher& dispatcher,
const Config& config, const RedisCommandStatsSharedPtr& redis_command_stats,
Stats::Scope& scope, const std::string& auth_password) override;
Stats::Scope& scope, const std::string& auth_username,
const std::string& auth_password) override;

static ClientFactoryImpl instance_;

Expand Down
12 changes: 12 additions & 0 deletions source/extensions/filters/network/common/redis/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ AuthRequest::AuthRequest(const std::string& password) {
asArray().swap(values);
}

AuthRequest::AuthRequest(const std::string& username, const std::string& password) {
std::vector<RespValue> values(3);
values[0].type(RespType::BulkString);
values[0].asString() = "auth";
values[1].type(RespType::BulkString);
values[1].asString() = username;
values[2].type(RespType::BulkString);
values[2].asString() = password;
type(RespType::Array);
asArray().swap(values);
}

RespValuePtr makeError(const std::string& error) {
Common::Redis::RespValuePtr response(new RespValue());
response->type(Common::Redis::RespType::Error);
Expand Down
1 change: 1 addition & 0 deletions source/extensions/filters/network/common/redis/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Utility {

class AuthRequest : public Redis::RespValue {
public:
AuthRequest(const std::string& username, const std::string& password);
AuthRequest(const std::string& password);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,18 @@ class SplitCallbacks {
virtual bool connectionAllowed() PURE;

/**
* Called when an authentication command has been received.
* Called when an authentication command has been received with a password.
* @param password supplies the AUTH password provided by the downstream client.
*/
virtual void onAuth(const std::string& password) PURE;

/**
* Called when an authentication command has been received with a username and password.
* @param username supplies the AUTH username provided by the downstream client.
* @param password supplies the AUTH password provided by the downstream client.
*/
virtual void onAuth(const std::string& username, const std::string& password) PURE;

/**
* Called when the response is ready.
* @param value supplies the response which is now owned by the callee.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,12 @@ SplitRequestPtr InstanceImpl::makeRequest(Common::Redis::RespValuePtr&& request,
onInvalidRequest(callbacks);
return nullptr;
}
callbacks.onAuth(request->asArray()[1].asString());
if (request->asArray().size() == 3) {
callbacks.onAuth(request->asArray()[1].asString(), request->asArray()[2].asString());
} else {
callbacks.onAuth(request->asArray()[1].asString());
}

return nullptr;
}

Expand Down
18 changes: 17 additions & 1 deletion source/extensions/filters/network/redis_proxy/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,27 @@ class ProtocolOptionsConfigImpl : public Upstream::ProtocolOptionsConfig {
ProtocolOptionsConfigImpl(
const envoy::extensions::filters::network::redis_proxy::v3::RedisProtocolOptions&
proto_config)
: auth_password_(proto_config.auth_password()) {}
: auth_username_(proto_config.auth_username()), auth_password_(proto_config.auth_password()) {
}

std::string authUsername(Api::Api& api) const {
return Config::DataSource::read(auth_username_, true, api);
}

std::string authPassword(Api::Api& api) const {
return Config::DataSource::read(auth_password_, true, api);
}

static const std::string authUsername(const Upstream::ClusterInfoConstSharedPtr info,
Api::Api& api) {
auto options = info->extensionProtocolOptionsTyped<ProtocolOptionsConfigImpl>(
NetworkFilterNames::get().RedisProxy);
if (options) {
return options->authUsername(api);
}
return EMPTY_STRING;
}

static const std::string authPassword(const Upstream::ClusterInfoConstSharedPtr info,
Api::Api& api) {
auto options = info->extensionProtocolOptionsTyped<ProtocolOptionsConfigImpl>(
Expand All @@ -41,6 +56,7 @@ class ProtocolOptionsConfigImpl : public Upstream::ProtocolOptionsConfig {
}

private:
envoy::config::core::v3::DataSource auth_username_;
envoy::config::core::v3::DataSource auth_password_;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ InstanceImpl::ThreadLocalPool::ThreadLocalPool(InstanceImpl& parent, Event::Disp
cluster_update_handle_ = parent_.cm_.addThreadLocalClusterUpdateCallbacks(*this);
Upstream::ThreadLocalCluster* cluster = parent_.cm_.get(cluster_name_);
if (cluster != nullptr) {
auth_username_ = ProtocolOptionsConfigImpl::authUsername(cluster->info(), parent_.api_);
auth_password_ = ProtocolOptionsConfigImpl::authPassword(cluster->info(), parent_.api_);
onClusterAddOrUpdateNonVirtual(*cluster);
}
Expand Down Expand Up @@ -214,9 +215,9 @@ InstanceImpl::ThreadLocalPool::threadLocalActiveClient(Upstream::HostConstShared
if (!client) {
client = std::make_unique<ThreadLocalActiveClient>(*this);
client->host_ = host;
client->redis_client_ = parent_.client_factory_.create(host, dispatcher_, parent_.config_,
parent_.redis_command_stats_,
*parent_.stats_scope_, auth_password_);
client->redis_client_ = parent_.client_factory_.create(
host, dispatcher_, parent_.config_, parent_.redis_command_stats_, *parent_.stats_scope_,
auth_username_, auth_password_);
client->redis_client_->addConnectionCallbacks(*client);
}
return client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class InstanceImpl : public Instance {
std::unordered_map<Upstream::HostConstSharedPtr, ThreadLocalActiveClientPtr> client_map_;
Envoy::Common::CallbackHandle* host_set_member_update_cb_handle_{};
std::unordered_map<std::string, Upstream::HostConstSharedPtr> host_address_map_;
std::string auth_username_;
std::string auth_password_;
std::list<Upstream::HostSharedPtr> created_via_redirect_hosts_;
std::list<ThreadLocalActiveClientPtr> clients_to_drain_;
Expand Down
30 changes: 29 additions & 1 deletion source/extensions/filters/network/redis_proxy/proxy_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ ProxyFilterConfig::ProxyFilterConfig(
: drain_decision_(drain_decision), runtime_(runtime),
stat_prefix_(fmt::format("redis.{}.", config.stat_prefix())),
stats_(generateStats(stat_prefix_, scope)),
downstream_auth_username_(
Config::DataSource::read(config.downstream_auth_username(), true, api)),
downstream_auth_password_(
Config::DataSource::read(config.downstream_auth_password(), true, api)) {}

Expand All @@ -38,7 +40,8 @@ ProxyFilter::ProxyFilter(Common::Redis::DecoderFactory& factory,
config_(config) {
config_->stats_.downstream_cx_total_.inc();
config_->stats_.downstream_cx_active_.inc();
connection_allowed_ = config_->downstream_auth_password_.empty();
connection_allowed_ =
config_->downstream_auth_username_.empty() && config_->downstream_auth_password_.empty();
}

ProxyFilter::~ProxyFilter() {
Expand Down Expand Up @@ -96,6 +99,31 @@ void ProxyFilter::onAuth(PendingRequest& request, const std::string& password) {
request.onResponse(std::move(response));
}

void ProxyFilter::onAuth(PendingRequest& request, const std::string& username,
const std::string& password) {
Common::Redis::RespValuePtr response{new Common::Redis::RespValue()};
if (config_->downstream_auth_username_.empty() && config_->downstream_auth_password_.empty()) {
response->type(Common::Redis::RespType::Error);
response->asString() = "ERR Client sent AUTH, but no username-password pair is set";
} else if (config_->downstream_auth_username_.empty() && username == "default" &&
password == config_->downstream_auth_password_) {
// empty username and "default" are synonymous in Redis 6 ACLs
response->type(Common::Redis::RespType::SimpleString);
response->asString() = "OK";
connection_allowed_ = true;
} else if (username == config_->downstream_auth_username_ &&
password == config_->downstream_auth_password_) {
response->type(Common::Redis::RespType::SimpleString);
response->asString() = "OK";
connection_allowed_ = true;
} else {
response->type(Common::Redis::RespType::Error);
response->asString() = "WRONGPASS invalid username-password pair";
connection_allowed_ = false;
}
request.onResponse(std::move(response));
}

void ProxyFilter::onResponse(PendingRequest& request, Common::Redis::RespValuePtr&& value) {
ASSERT(!pending_requests_.empty());
request.pending_response_ = std::move(value);
Expand Down
Loading