Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/acl.c
Original file line number Diff line number Diff line change
Expand Up @@ -2575,6 +2575,8 @@ static void ACLUpdateInfoMetrics(int reason) {
server.acl_info.invalid_key_accesses++;
} else if (reason == ACL_DENIED_CHANNEL) {
server.acl_info.invalid_channel_accesses++;
} else if (reason == ACL_INVALID_TLS_CERT_AUTH) {
server.acl_info.acl_access_denied_tls_cert++;
} else {
serverPanic("Unknown ACL_DENIED encoding");
}
Expand Down Expand Up @@ -3016,6 +3018,7 @@ void aclCommand(client *c) {
case ACL_DENIED_KEY: reasonstr = "key"; break;
case ACL_DENIED_CHANNEL: reasonstr = "channel"; break;
case ACL_DENIED_AUTH: reasonstr = "auth"; break;
case ACL_INVALID_TLS_CERT_AUTH: reasonstr = "tls-cert"; break;
default: reasonstr = "unknown";
}
addReplyBulkCString(c, reasonstr);
Expand Down
7 changes: 7 additions & 0 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ configEnum tls_auth_clients_enum[] = {
{"optional", TLS_CLIENT_AUTH_OPTIONAL},
{NULL, 0}};

configEnum tls_client_auth_user_enum[] = {
{"CN", TLS_CLIENT_FIELD_CN},
{"off", TLS_CLIENT_FIELD_OFF},
{NULL, 0} // terminator
};

configEnum oom_score_adj_enum[] = {
{"no", OOM_SCORE_ADJ_NO},
{"yes", OOM_SCORE_RELATIVE},
Expand Down Expand Up @@ -3369,6 +3375,7 @@ standardConfig static_configs[] = {
createBoolConfig("tls-cluster", NULL, MODIFIABLE_CONFIG, server.tls_cluster, 0, NULL, applyTlsCfg),
createBoolConfig("tls-replication", NULL, MODIFIABLE_CONFIG, server.tls_replication, 0, NULL, applyTlsCfg),
createEnumConfig("tls-auth-clients", NULL, MODIFIABLE_CONFIG, tls_auth_clients_enum, server.tls_auth_clients, TLS_CLIENT_AUTH_YES, NULL, NULL),
createEnumConfig("tls-auth-clients-user", NULL, MODIFIABLE_CONFIG, tls_client_auth_user_enum, server.tls_ctx_config.client_auth_user, TLS_CLIENT_FIELD_OFF, NULL, NULL),
createBoolConfig("tls-prefer-server-ciphers", NULL, MODIFIABLE_CONFIG, server.tls_ctx_config.prefer_server_ciphers, 0, NULL, applyTlsCfg),
createBoolConfig("tls-session-caching", NULL, MODIFIABLE_CONFIG, server.tls_ctx_config.session_caching, 1, NULL, applyTlsCfg),
createStringConfig("tls-cert-file", NULL, VOLATILE_CONFIG | MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.cert_file, NULL, NULL, applyTlsCfg),
Expand Down
11 changes: 11 additions & 0 deletions src/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ typedef struct ConnectionType {
/* TLS specified methods */
sds (*get_peer_cert)(struct connection *conn);

/* Get peer username based on connection type */
sds (*get_peer_username)(connection *conn);

/* Miscellaneous */
int (*connIntegrityChecked)(void); // return 1 if connection type has built-in integrity checks
} ConnectionType;
Expand Down Expand Up @@ -416,6 +419,14 @@ static inline sds connGetPeerCert(connection *conn) {
return NULL;
}

/* Get Peer username based on connection type */
static inline sds connGetPeerUsername(connection *conn) {
if (conn->type && conn->type->get_peer_username) {
return conn->type->get_peer_username(conn);
}
return NULL;
}

/* Initialize the connection framework */
int connTypeInitialize(void);

Expand Down
16 changes: 16 additions & 0 deletions src/networking.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "fmtargs.h"
#include "io_threads.h"
#include "module.h"
#include "connection.h"
#include <strings.h>
#include <sys/socket.h>
#include <sys/uio.h>
Expand Down Expand Up @@ -1682,6 +1683,21 @@ void clientAcceptHandler(connection *conn) {
}
}

/* Auto-authenticate from cert_user field if set */
sds username = connGetPeerUsername(conn);
if (username != NULL) {
user *u = ACLGetUserByName(username, sdslen(username));
if (u) {
c->user = u;
c->flag.authenticated = true;
serverLog(LL_VERBOSE, "TLS: Auto-authenticated client as %s",
server.hide_user_data_from_log ? "*redacted*" : u->name);
} else {
addACLLogEntry(c, ACL_INVALID_TLS_CERT_AUTH, ACL_LOG_CTX_TOPLEVEL, 0, username, NULL);
}
sdsfree(username);
}

server.stat_numconnections++;
moduleFireServerEvent(VALKEYMODULE_EVENT_CLIENT_CHANGE, VALKEYMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED, c);
}
Expand Down
7 changes: 5 additions & 2 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -2946,6 +2946,7 @@ void initServer(void) {
server.acl_info.invalid_key_accesses = 0;
server.acl_info.user_auth_failures = 0;
server.acl_info.invalid_channel_accesses = 0;
server.acl_info.acl_access_denied_tls_cert = 0;

/* Create the timer callback, this is our way to process many background
* operations incrementally, like eviction of unaccessed expired keys, etc. */
Expand Down Expand Up @@ -5584,9 +5585,11 @@ sds genValkeyInfoStringACLStats(sds info) {
"acl_access_denied_auth:%lld\r\n"
"acl_access_denied_cmd:%lld\r\n"
"acl_access_denied_key:%lld\r\n"
"acl_access_denied_channel:%lld\r\n",
"acl_access_denied_channel:%lld\r\n"
"acl_access_denied_tls_cert:%lld\r\n",
server.acl_info.user_auth_failures, server.acl_info.invalid_cmd_accesses,
server.acl_info.invalid_key_accesses, server.acl_info.invalid_channel_accesses);
server.acl_info.invalid_key_accesses, server.acl_info.invalid_channel_accesses,
server.acl_info.acl_access_denied_tls_cert);
return info;
}

Expand Down
19 changes: 13 additions & 6 deletions src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ typedef enum {
#define TLS_CLIENT_AUTH_YES 1
#define TLS_CLIENT_AUTH_OPTIONAL 2

/* TLS Client Certfiicate Authentication */
#define TLS_CLIENT_FIELD_OFF 0
#define TLS_CLIENT_FIELD_CN 1

/* Sanitize dump payload */
#define SANITIZE_DUMP_NO 0
#define SANITIZE_DUMP_YES 1
Expand Down Expand Up @@ -1311,10 +1315,11 @@ typedef struct writePreparedClient writePreparedClient;

/* ACL information */
typedef struct aclInfo {
long long user_auth_failures; /* Auth failure counts on user level */
long long invalid_cmd_accesses; /* Invalid command accesses that user doesn't have permission to */
long long invalid_key_accesses; /* Invalid key accesses that user doesn't have permission to */
long long invalid_channel_accesses; /* Invalid channel accesses that user doesn't have permission to */
long long user_auth_failures; /* Auth failure counts on user level */
long long invalid_cmd_accesses; /* Invalid command accesses that user doesn't have permission to */
long long invalid_key_accesses; /* Invalid key accesses that user doesn't have permission to */
long long invalid_channel_accesses; /* Invalid channel accesses that user doesn't have permission to */
long long acl_access_denied_tls_cert; /* TLS clients with cert not matching any existing user. */
} aclInfo;

struct saveparam {
Expand Down Expand Up @@ -1509,6 +1514,7 @@ typedef struct serverTLSContextConfig {
char *client_cert_file; /* Certificate to use as a client; if none, use cert_file */
char *client_key_file; /* Private key filename for client_cert_file */
char *client_key_file_pass; /* Optional password for client_key_file */
int client_auth_user; /* Field to be used for automatic TLS authentication based on client TLS certificate */
char *dh_params_file;
char *ca_cert_file;
char *ca_cert_dir;
Expand Down Expand Up @@ -3069,8 +3075,9 @@ void ACLInit(void);
#define ACL_OK 0
#define ACL_DENIED_CMD 1
#define ACL_DENIED_KEY 2
#define ACL_DENIED_AUTH 3 /* Only used for ACL LOG entries. */
#define ACL_DENIED_CHANNEL 4 /* Only used for pub/sub commands */
#define ACL_DENIED_AUTH 3 /* Only used for ACL LOG entries. */
#define ACL_DENIED_CHANNEL 4 /* Only used for pub/sub commands */
#define ACL_INVALID_TLS_CERT_AUTH 5 /* Only used for TLS Auto-authentication */

/* Context values for addACLLogEntry(). */
#define ACL_LOG_CTX_TOPLEVEL 0
Expand Down
54 changes: 54 additions & 0 deletions src/tls.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

#include <openssl/conf.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/pem.h>
Expand Down Expand Up @@ -679,6 +680,57 @@ static void updateSSLState(connection *conn_) {
updatePendingData(conn);
}

static int getCertFieldByName(X509 *cert, const char *field, char *out, size_t outlen) {
if (!cert || !field || !out) return 0;

int nid = -1;

if (!strcasecmp(field, "CN"))
nid = NID_commonName;
else if (!strcasecmp(field, "O"))
nid = NID_organizationName;
/* Add more mappings here as needed */

if (nid == -1) return 0;

X509_NAME *subject = X509_get_subject_name(cert);
if (!subject) return 0;

return X509_NAME_get_text_by_NID(subject, nid, out, outlen) > 0;
}

sds tlsGetPeerUsername(connection *conn_) {
tls_connection *conn = (tls_connection *)conn_;
if (!conn || !SSL_is_init_finished(conn->ssl)) return NULL;

/* Find the corresponding field name from the enum mapping */
const char *field = NULL;
switch (server.tls_ctx_config.client_auth_user) {
case TLS_CLIENT_FIELD_CN:
field = "CN";
break;
default:
return NULL;
}

if (!field) return NULL;

X509 *cert = SSL_get_peer_certificate(conn->ssl);
if (!cert) return NULL;

char field_value[256];
sds result = NULL;

if (getCertFieldByName(cert, field, field_value, sizeof(field_value))) {
result = sdsnew(field_value);
} else {
serverLog(LL_NOTICE, "TLS: Failed to extract field '%s' from certificate", field);
}

X509_free(cert);
return result;
}

static void TLSAccept(void *_conn) {
tls_connection *conn = (tls_connection *)_conn;
ERR_clear_error();
Expand Down Expand Up @@ -1218,9 +1270,11 @@ static ConnectionType CT_TLS = {

/* TLS specified methods */
.get_peer_cert = connTLSGetPeerCert,
.get_peer_username = tlsGetPeerUsername,

/* Miscellaneous */
.connIntegrityChecked = connTLSIsIntegrityChecked,

};

int RedisRegisterConnectionTypeTLS(void) {
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/tls.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,23 @@ start_server {tags {"tls"}} {
r config set tls-key-file-pass 1234
r config set tls-key-file $keyfile_encrypted
}

test {TLS: Auto-authenticate using tls-auth-clients-user (CN)} {
# Create a user matching the CN in the client certificate (CN=Client-only)
r ACL SETUSER {Client-only} on >clientpass allcommands allkeys

# Enable the feature to auto-authenticate based on CN
r CONFIG SET tls-auth-clients-user CN

# With feature on, client should be auto-authenticated using CN=Client-only
set s [valkey [srv 0 host] [srv 0 port] 0 1]
::tls::import [$s channel]

# Now no explicit AUTH is needed
assert_equal "PONG" [$s PING]

# Verify that the authenticated user is 'Client-only'
assert_equal "Client-only" [$s ACL WHOAMI]
}
}
}
14 changes: 14 additions & 0 deletions valkey.conf
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,20 @@ tcp-keepalive 300
# tls-auth-clients no
# tls-auth-clients optional

# Automatically authenticate TLS clients as Valkey users based on their
# certificates.
#
# If set to a field like "CN", the server will extract the corresponding field
# from the client's TLS certificate and attempt to find a Valkey user with the
# same name. If a matching user is found, the client is automatically
# authenticated as that user during the TLS handshake. If no matching user is
# found, the client is connected as the unauthenticated default user. Set to
# "off" to disable automatic user authentication via certificate fields.
#
# Supported values: CN, off. Default: off.
#
# tls-auth-clients-user CN

# By default, a replica does not attempt to establish a TLS connection
# with its primary.
#
Expand Down
Loading