redis: Add authentication support

Add authentication support to the Redis logging output.
It introduces `username` and `password` configuration options for Redis,
allowing Suricata to authenticate with Redis servers that require it.

Ticket: 7062
pull/14739/head
Fupeng Zhao 5 months ago committed by Victor Julien
parent e982dd50d0
commit f7ec1c8813

@ -76,6 +76,8 @@ Output types::
#redis:
# server: 127.0.0.1
# port: 6379
# username: null ## ACL username; if null (default), no username is used
# password: null ## AUTH password; if null (default), authentication is disabled
# async: true ## if redis replies are read asynchronously
# mode: list ## possible values: list|lpush (default), rpush, channel|publish, xadd|stream
# ## lpush and rpush are using a Redis list. "list" is an alias for lpush

@ -21,6 +21,8 @@ outputs:
#redis:
# server: 127.0.0.1
# port: 6379
# username: null ## ACL username; if null (default), no username is used
# password: null ## AUTH password; if null (default), authentication is disabled
# async: true ## if redis replies are read asynchronously
# mode: list ## possible values: list|lpush (default), rpush, channel|publish, xadd|stream
# ## lpush and rpush are using a Redis list. "list" is an alias for lpush

@ -96,7 +96,7 @@ static SCLogRedisContext *SCLogRedisContextAsyncAlloc(void)
ctx->sync = NULL;
ctx->async = NULL;
ctx->ev_base = NULL;
ctx->connected = 0;
ctx->state = REDIS_STATE_DISCONNECTED;
ctx->batch_count = 0;
ctx->last_push = 0;
ctx->tried = 0;
@ -116,15 +116,69 @@ static void SCRedisAsyncCommandCallback(redisAsyncContext *ac, void *r, void *pr
SCLogRedisContext *ctx = log_ctx->redis;
if (reply == NULL) {
if (ctx->connected > 0)
if (ctx->state != REDIS_STATE_DISCONNECTED)
SCLogInfo("Missing reply from redis, disconnected.");
ctx->connected = 0;
ctx->state = REDIS_STATE_DISCONNECTED;
} else {
ctx->connected = 1;
event_base_loopbreak(ctx->ev_base);
}
}
/** \brief SCRedisAsyncAuthCallback() Callback when AUTH reply from redis happens.
* \param ac redis async context
* \param r redis reply
* \param privdata opaque data with pointer to LogFileCtx
*/
static void SCRedisAsyncAuthCallback(redisAsyncContext *ac, void *r, void *privdata)
{
redisReply *reply = r;
LogFileCtx *log_ctx = privdata;
SCLogRedisContext *ctx = log_ctx->redis;
if (reply == NULL) {
if (ctx->tried == 0) {
SCLogWarning("Failed to connect to Redis... (will keep trying)");
}
ctx->state = REDIS_STATE_DISCONNECTED;
ctx->tried = time(NULL);
} else {
if (reply->type != REDIS_REPLY_ERROR) {
SCLogInfo("Redis authenticated successfully.");
ctx->state = REDIS_STATE_AUTHENTICATED;
ctx->tried = 0;
} else {
if (ctx->tried == 0) {
SCLogWarning("Redis AUTH failed: %s (will keep trying)", reply->str);
}
ctx->state = REDIS_STATE_AUTH_FAILED;
ctx->tried = time(NULL);
}
}
event_base_loopbreak(ctx->ev_base);
}
/** \brief SCLogAsyncRedisSendAuth() - Authenticates with redis
* \param log_ctx Log file context allocated by caller
*/
static void SCLogAsyncRedisSendAuth(LogFileCtx *log_ctx)
{
SCLogRedisContext *ctx = log_ctx->redis;
/* only try to reauth once per second */
if (ctx->tried >= time(NULL)) {
return;
}
if (log_ctx->redis_setup.username != NULL) {
redisAsyncCommand(ctx->async, SCRedisAsyncAuthCallback, log_ctx, "AUTH %s %s",
log_ctx->redis_setup.username, log_ctx->redis_setup.password);
} else {
redisAsyncCommand(ctx->async, SCRedisAsyncAuthCallback, log_ctx, "AUTH %s",
log_ctx->redis_setup.password);
}
event_base_dispatch(ctx->ev_base);
}
/** \brief SCRedisAsyncEchoCommandCallback() Callback for an ECHO command reply
* This is used to check if redis is connected.
* \param ac redis async context
@ -136,24 +190,35 @@ static void SCRedisAsyncEchoCommandCallback(redisAsyncContext *ac, void *r, void
redisReply *reply = r;
SCLogRedisContext * ctx = privdata;
if (reply) {
if (ctx->connected == 0) {
SCLogNotice("Connected to Redis.");
ctx->connected = 1;
ctx->tried = 0;
}
if (reply == NULL) {
if (ctx->tried == 0) {
SCLogWarning("Failed to connect to Redis... (will keep trying)");
}
ctx->state = REDIS_STATE_DISCONNECTED;
ctx->tried = time(NULL);
} else {
ctx->connected = 0;
if (ctx->tried == 0) {
SCLogWarning("Failed to connect to Redis... (will keep trying)");
}
ctx->tried = time(NULL);
if (reply->type != REDIS_REPLY_ERROR) {
SCLogNotice("Connected to Redis.");
ctx->state = REDIS_STATE_CONNECTED;
ctx->tried = 0;
} else {
if (strncmp(reply->str, "NOAUTH", 6) == 0) {
if (ctx->tried == 0) {
SCLogWarning("Redis authentication required, but not configured.");
}
} else {
if (ctx->tried == 0) {
SCLogWarning("Redis ECHO command failed: %s", reply->str);
}
}
ctx->state = REDIS_STATE_ECHO_FAILED;
ctx->tried = time(NULL);
}
}
event_base_loopbreak(ctx->ev_base);
}
/** \brief SCRedisAsyncEchoCommandCallback() Callback for an QUIT command reply
* Emits and awaits response for an async ECHO command.
/** \brief SCLogAsyncRedisSendEcho() - Emits and awaits response for an async ECHO command.
* It's used for check if redis is alive.
* \param ctx redis context
*/
@ -181,7 +246,7 @@ static void SCRedisAsyncQuitCommandCallback(redisAsyncContext *ac, void *r, void
*/
static void SCLogAsyncRedisSendQuit(SCLogRedisContext * ctx)
{
if (ctx->connected) {
if (ctx->state != REDIS_STATE_DISCONNECTED) {
redisAsyncCommand(ctx->async, SCRedisAsyncQuitCommandCallback, ctx, "QUIT");
SCLogInfo("QUIT Command sent to redis. Connection will terminate!");
}
@ -191,7 +256,7 @@ static void SCLogAsyncRedisSendQuit(SCLogRedisContext * ctx)
ctx->async = NULL;
event_base_free(ctx->ev_base);
ctx->ev_base = NULL;
ctx->connected = 0;
ctx->state = REDIS_STATE_DISCONNECTED;
}
/** \brief SCConfLogReopenAsyncRedis() Open or re-opens connection to redis for logging.
@ -247,6 +312,16 @@ static int SCConfLogReopenAsyncRedis(LogFileCtx *log_ctx)
return 0;
}
/** \brief SCLogAsyncRedisIsReady() Determines whether is ready to send data.
* \param file_ctx Log file context allocated by caller
*/
static inline bool SCLogAsyncRedisIsReady(LogFileCtx *file_ctx)
{
SCLogRedisContext *ctx = file_ctx->redis;
return file_ctx->redis_setup.password ? ctx->state == REDIS_STATE_AUTHENTICATED
: ctx->state == REDIS_STATE_CONNECTED;
}
/** \brief SCLogRedisWriteAsync() writes string to redis output in async mode
* \param file_ctx Log file context allocated by caller
@ -256,17 +331,25 @@ static int SCLogRedisWriteAsync(LogFileCtx *file_ctx, const char *string, size_t
{
SCLogRedisContext *ctx = file_ctx->redis;
if (! ctx->connected) {
if (SCConfLogReopenAsyncRedis(file_ctx) == -1) {
return -1;
if (!SCLogAsyncRedisIsReady(file_ctx)) {
if (ctx->state == REDIS_STATE_DISCONNECTED) {
if (SCConfLogReopenAsyncRedis(file_ctx) == -1) {
return -1;
}
}
if (ctx->tried == 0) {
SCLogNotice("Trying to connect to Redis");
SCLogNotice("Trying to connect to Redis");
}
if (file_ctx->redis_setup.password == NULL) {
// Just verify the connection is alive with ECHO
SCLogAsyncRedisSendEcho(ctx);
} else {
// Send AUTH to authenticate and verify the connection is alive
SCLogAsyncRedisSendAuth(file_ctx);
}
SCLogAsyncRedisSendEcho(ctx);
}
if (!ctx->connected) {
if (!SCLogAsyncRedisIsReady(file_ctx)) {
return -1;
}
@ -323,6 +406,47 @@ static int SCConfLogReopenSyncRedis(LogFileCtx *log_ctx)
}
SCLogInfo("Connected to redis server [%s].", log_ctx->redis_setup.server);
if (log_ctx->redis_setup.password != NULL) {
redisReply *reply;
if (log_ctx->redis_setup.username != NULL) {
reply = redisCommand(ctx->sync, "AUTH %s %s", log_ctx->redis_setup.username,
log_ctx->redis_setup.password);
} else {
reply = redisCommand(ctx->sync, "AUTH %s", log_ctx->redis_setup.password);
}
if (reply == NULL || reply->type == REDIS_REPLY_ERROR) {
SCLogWarning("Redis AUTH failed: %s", reply ? reply->str : ctx->sync->errstr);
if (reply) {
freeReplyObject(reply);
}
redisFree(ctx->sync);
ctx->sync = NULL;
ctx->tried = time(NULL);
return -1;
}
freeReplyObject(reply);
SCLogInfo("Redis authenticated successfully.");
}
/* Check if we are really ready to write logs */
redisReply *reply = redisCommand(ctx->sync, "ECHO suricata");
if (reply == NULL || reply->type == REDIS_REPLY_ERROR) {
if (reply != NULL && strncmp(reply->str, "NOAUTH", 6) == 0) {
SCLogWarning("Redis authentication required, but not configured.");
} else {
SCLogWarning("Redis ECHO failed: %s", reply ? reply->str : ctx->sync->errstr);
}
if (reply) {
freeReplyObject(reply);
}
redisFree(ctx->sync);
ctx->sync = NULL;
ctx->tried = time(NULL);
return -1;
}
freeReplyObject(reply);
log_ctx->redis = ctx;
log_ctx->Close = SCLogFileCloseRedis;
return 0;
@ -473,6 +597,8 @@ int SCConfLogOpenRedis(SCConfNode *redis_node, void *lf_ctx)
if (redis_node) {
log_ctx->redis_setup.server = SCConfNodeLookupChildValue(redis_node, "server");
log_ctx->redis_setup.key = SCConfNodeLookupChildValue(redis_node, "key");
log_ctx->redis_setup.username = SCConfNodeLookupChildValue(redis_node, "username");
log_ctx->redis_setup.password = SCConfNodeLookupChildValue(redis_node, "password");
redis_port = SCConfNodeLookupChildValue(redis_node, "port");
redis_mode = SCConfNodeLookupChildValue(redis_node, "mode");
@ -490,6 +616,10 @@ int SCConfLogOpenRedis(SCConfNode *redis_node, void *lf_ctx)
if (!log_ctx->redis_setup.key) {
log_ctx->redis_setup.key = redis_default_key;
}
if (log_ctx->redis_setup.username && !log_ctx->redis_setup.password) {
SCLogWarning("Redis username configured without password; ignoring username.");
log_ctx->redis_setup.username = NULL;
}
#ifndef HAVE_LIBEVENT
if (is_async) {
@ -585,7 +715,7 @@ void SCLogFileCloseRedis(LogFileCtx *log_ctx)
if (log_ctx->redis_setup.is_async) {
#if HAVE_LIBEVENT == 1
if (ctx->async) {
if (ctx->connected > 0) {
if (ctx->state != REDIS_STATE_DISCONNECTED) {
SCLogAsyncRedisSendQuit(ctx);
}
if (ctx->ev_base != NULL) {

@ -41,6 +41,8 @@ typedef struct RedisSetup_ {
const char *format;
const char *command;
const char *key;
const char *username;
const char *password;
const char *server;
uint16_t port;
int is_async;
@ -48,12 +50,34 @@ typedef struct RedisSetup_ {
char *stream_format;
} RedisSetup;
#if HAVE_LIBEVENT
enum RedisConnState {
/** The context is not connected to the Redis server. */
REDIS_STATE_DISCONNECTED,
/** The ECHO command failed, possibly due to requiring authentication. */
REDIS_STATE_ECHO_FAILED,
/** A connection to the Redis server has been established.
* If authentication is not required, this is the final state where data can be safely sent.
* If authentication is required, the client must proceed to authenticate before sending data.
*/
REDIS_STATE_CONNECTED,
/** Authentication with the Redis server failed. */
REDIS_STATE_AUTH_FAILED,
/** The connection is fully authenticated, and ready to send data. */
REDIS_STATE_AUTHENTICATED,
};
#endif /* HAVE_LIBEVENT */
typedef struct SCLogRedisContext_ {
redisContext *sync;
#if HAVE_LIBEVENT
redisAsyncContext *async;
struct event_base *ev_base;
int connected;
enum RedisConnState state;
#endif /* HAVE_LIBEVENT */
time_t tried;
int batch_count;

@ -116,6 +116,8 @@ outputs:
#redis:
# server: 127.0.0.1
# port: 6379
# username: null ## ACL username; if null (default), no username is used
# password: null ## AUTH password; if null (default), authentication is disabled
# async: true ## if redis replies are read asynchronously
# mode: list ## possible values: list|lpush (default), rpush, channel|publish, xadd|stream
# ## lpush and rpush are using a Redis list. "list" is an alias for lpush

Loading…
Cancel
Save