Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -3372,6 +3378,7 @@ standardConfig static_configs[] = {
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),
createEnumConfig("tls-auth-clients-user", NULL, VOLATILE_CONFIG | MODIFIABLE_CONFIG, tls_client_auth_user_enum, server.tls_ctx_config.client_auth_user, TLS_CLIENT_FIELD_OFF, NULL, applyTlsCfg),
createStringConfig("tls-key-file", NULL, VOLATILE_CONFIG | MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.key_file, NULL, NULL, applyTlsCfg),
createStringConfig("tls-key-file-pass", NULL, MODIFIABLE_CONFIG | SENSITIVE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.key_file_pass, NULL, NULL, applyTlsCfg),
createStringConfig("tls-client-cert-file", NULL, VOLATILE_CONFIG | MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.client_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]
}
}
}
15 changes: 15 additions & 0 deletions valkey.conf
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,21 @@ tcp-keepalive 300
#
# tls-session-cache-timeout 60

# Automatically authenticate TLS clients as Valkey users based on their certificate.
#
# 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 server falls back to the default user.
#
# Set to "off" to disable automatic user authentication via certificate fields.
#
# Supported values: CN, off. Default: off.
#
# tls-auth-clients-user CN

################################### RDMA ######################################

# Valkey Over RDMA is experimental, it may be changed or be removed in any minor or major version.
Expand Down
Loading