diff --git a/src/acl.c b/src/acl.c index c33b5d1ed7..fd775c3ec0 100644 --- a/src/acl.c +++ b/src/acl.c @@ -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"); } @@ -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); diff --git a/src/config.c b/src/config.c index f26fba05d1..2fc9c8660c 100644 --- a/src/config.c +++ b/src/config.c @@ -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}, @@ -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), diff --git a/src/connection.h b/src/connection.h index a0bdba232e..5045477d37 100644 --- a/src/connection.h +++ b/src/connection.h @@ -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; @@ -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); diff --git a/src/networking.c b/src/networking.c index 829b410c86..94f97ebae2 100644 --- a/src/networking.c +++ b/src/networking.c @@ -37,6 +37,7 @@ #include "fmtargs.h" #include "io_threads.h" #include "module.h" +#include "connection.h" #include #include #include @@ -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); } diff --git a/src/server.c b/src/server.c index f5a44e6e15..a5610a7008 100644 --- a/src/server.c +++ b/src/server.c @@ -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. */ @@ -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; } diff --git a/src/server.h b/src/server.h index e5e8141e00..8d96790e64 100644 --- a/src/server.h +++ b/src/server.h @@ -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 @@ -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 { @@ -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; @@ -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 diff --git a/src/tls.c b/src/tls.c index e5af01b2fa..8d45a42189 100644 --- a/src/tls.c +++ b/src/tls.c @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -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(); @@ -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) { diff --git a/tests/unit/tls.tcl b/tests/unit/tls.tcl index 91c25a86c2..395683c02b 100644 --- a/tests/unit/tls.tcl +++ b/tests/unit/tls.tcl @@ -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] + } } } diff --git a/valkey.conf b/valkey.conf index d4b5450074..ea80c72719 100644 --- a/valkey.conf +++ b/valkey.conf @@ -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. #