mirror of
https://github.com/valkey-io/valkey.git
synced 2026-05-06 05:26:42 -04:00
Support TLS authentication using SAN URI (#3078)
Closes https://github.com/valkey-io/valkey/issues/3077 ### Overview URI in SAN is used to represent client identities in modern mTLS deployments where CN may be empty or deprecated. See the https://github.com/valkey-io/valkey/issues/3077#issuecomment-3775615304 for more details. When `tls-auth-clients-user URI` is configured, during the TLS handshake, the server iterates through the URIs in the client certificate and authenticates the client as the first enabled user whose name matches one of those URIs. ### Implementation - Introduced a new value `URI` for `tls-auth-clients-user` - Added new function `getCertSanUri` that: - Extracts URI entries from the certificate's SAN extension - Checks each URI against existing Valkey users - Returns the first URI that matches an enabled user - Renamed `getCertFieldByName` → `getCertSubjectFieldByName` for clarity - Modified `tlsGetPeerUsername` to support both CN and URI authentication modes ### Example behavior Common setup ``` # client certificate X509v3 Subject Alternative Name URI:urn:valkey:user:first, URI:urn:valkey:user:second # valkey.conf tls-auth-clients-user URI hide-user-data-from-log no ``` Use case 1: multiple enabled users ``` user urn:valkey:user:first on >clientpass allcommands allkeys user urn:valkey:user:second on >clientpass allcommands allkeys 39762:M 26 Jan 2026 22:06:25.122 - TLS: Auto-authenticated client as urn:valkey:user:first ``` Use case 2: first URI disabled, second enabled ``` user urn:valkey:user:first off >clientpass allcommands allkeys user urn:valkey:user:second on >clientpass allcommands allkeys 39792:M 26 Jan 2026 22:07:08.006 - TLS: Auto-authenticated client as urn:valkey:user:second ``` Use case 3: all matching users disabled or no matching user ``` user urn:valkey:user:first off >clientpass allcommands allkeys user urn:valkey:user:second off >clientpass allcommands allkeys 39812:M 26 Jan 2026 22:07:34.174 * TLS: No matching user found in certificate SAN URI fields 127.0.0.1:6379> acl whoami "default" 127.0.0.1:6379> acl log 1) 1) "count" 2) (integer) 1 3) "reason" 4) "tls-cert" 5) "context" 6) "toplevel" 7) "object" 8) "" 9) "username" 10) "urn:valkey:user:second" 11) "age-seconds" 12) "17.381" 13) "client-info" 14) "id=3 addr=127.0.0.1:57236 laddr=127.0.0.1:6379 fd=8 name= age=0 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=NULL user=default redir=-1 resp=2 lib-name= lib-ver= tot-net-in=0 tot-net-out=0 tot-cmds=0" 15) "entry-id" 16) (integer) 0 17) "timestamp-created" 18) (integer) 1771963041866 19) "timestamp-last-updated" 20) (integer) 1771963041866 127.0.0.1:6379> ``` --------- Signed-off-by: Yang Zhao <zymy701@gmail.com>
This commit is contained in:
+2
-2
@@ -126,9 +126,9 @@ configEnum tls_auth_clients_enum[] = {
|
||||
|
||||
configEnum tls_client_auth_user_enum[] = {
|
||||
{"CN", TLS_CLIENT_FIELD_CN},
|
||||
{"URI", TLS_CLIENT_FIELD_URI},
|
||||
{"off", TLS_CLIENT_FIELD_OFF},
|
||||
{NULL, 0} // terminator
|
||||
};
|
||||
{NULL, 0}};
|
||||
|
||||
configEnum oom_score_adj_enum[] = {
|
||||
{"no", OOM_SCORE_ADJ_NO},
|
||||
|
||||
+9
-6
@@ -48,6 +48,7 @@
|
||||
#define NET_HOST_PORT_STR_LEN (NET_HOST_STR_LEN + 32) /* Must be enough for hostname:port */
|
||||
|
||||
struct aeEventLoop;
|
||||
struct user;
|
||||
typedef struct connection connection;
|
||||
typedef struct connListener connListener;
|
||||
|
||||
@@ -147,8 +148,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);
|
||||
/* Get peer user based on connection type. If cert_username is non-NULL,
|
||||
* it is set to the extracted certificate field value. */
|
||||
struct user *(*get_peer_user)(connection *conn, sds *cert_username);
|
||||
|
||||
/* Miscellaneous */
|
||||
int (*connIntegrityChecked)(void); // return 1 if connection type has built-in integrity checks
|
||||
@@ -424,10 +426,11 @@ 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);
|
||||
/* Get peer user based on connection type. If cert_username is non-NULL,
|
||||
* it is set to the extracted certificate field value. */
|
||||
static inline struct user *connGetPeerUser(connection *conn, sds *cert_username) {
|
||||
if (conn->type && conn->type->get_peer_user) {
|
||||
return conn->type->get_peer_user(conn, cert_username);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+11
-13
@@ -1772,20 +1772,18 @@ 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 && (u->flags & USER_FLAG_ENABLED)) {
|
||||
clientSetUser(c, u, 1);
|
||||
moduleNotifyUserChanged(c);
|
||||
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);
|
||||
/* Auto-authenticate from cert user field if set */
|
||||
sds cert_username = NULL;
|
||||
user *u = connGetPeerUser(conn, &cert_username);
|
||||
if (u) {
|
||||
clientSetUser(c, u, 1);
|
||||
moduleNotifyUserChanged(c);
|
||||
serverLog(LL_VERBOSE, "TLS: Auto-authenticated client as %s",
|
||||
server.hide_user_data_from_log ? "*redacted*" : u->name);
|
||||
} else if (cert_username) {
|
||||
addACLLogEntry(c, ACL_INVALID_TLS_CERT_AUTH, ACL_LOG_CTX_TOPLEVEL, 0, cert_username, NULL);
|
||||
}
|
||||
sdsfree(cert_username);
|
||||
|
||||
server.stat_numconnections++;
|
||||
moduleFireServerEvent(VALKEYMODULE_EVENT_CLIENT_CHANGE, VALKEYMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED, c);
|
||||
|
||||
+3
-2
@@ -554,9 +554,10 @@ typedef enum {
|
||||
#define TLS_CLIENT_AUTH_YES 1
|
||||
#define TLS_CLIENT_AUTH_OPTIONAL 2
|
||||
|
||||
/* TLS Client Certfiicate Authentication */
|
||||
/* TLS Client Certificate Authentication */
|
||||
#define TLS_CLIENT_FIELD_OFF 0
|
||||
#define TLS_CLIENT_FIELD_CN 1
|
||||
#define TLS_CLIENT_FIELD_URI 2
|
||||
|
||||
/* Sanitize dump payload */
|
||||
#define SANITIZE_DUMP_NO 0
|
||||
@@ -1068,7 +1069,7 @@ typedef struct readyList {
|
||||
#define SELECTOR_FLAG_ALLDBS (1 << 4) /* Allow all databases */
|
||||
|
||||
|
||||
typedef struct {
|
||||
typedef struct user {
|
||||
sds name; /* The username as an SDS string. */
|
||||
uint32_t flags; /* See USER_FLAG_* */
|
||||
list *passwords; /* A list of SDS valid passwords for this user. */
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <openssl/conf.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
@@ -1277,7 +1278,7 @@ static void updateSSLState(connection *conn_) {
|
||||
updatePendingData(conn);
|
||||
}
|
||||
|
||||
static int getCertFieldByName(X509 *cert, const char *field, char *out, size_t outlen) {
|
||||
static int getCertSubjectFieldByName(X509 *cert, const char *field, char *out, size_t outlen) {
|
||||
if (!cert || !field || !out) return 0;
|
||||
|
||||
int nid = -1;
|
||||
@@ -1296,35 +1297,91 @@ static int getCertFieldByName(X509 *cert, const char *field, char *out, size_t o
|
||||
return X509_NAME_get_text_by_NID(subject, nid, out, outlen) > 0;
|
||||
}
|
||||
|
||||
sds tlsGetPeerUsername(connection *conn_) {
|
||||
/* Extract URI from Subject Alternative Name extension and return the first
|
||||
* enabled Valkey user that matches a URI. Returns NULL if no match found.
|
||||
* If cert_username is non-NULL, it is set to the last URI checked. */
|
||||
static user *getValidUserFromCertSanUri(X509 *cert, sds *cert_username) {
|
||||
if (!cert) return NULL;
|
||||
|
||||
GENERAL_NAMES *san_names = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
|
||||
if (!san_names) return NULL;
|
||||
|
||||
user *result = NULL;
|
||||
int num_names = sk_GENERAL_NAME_num(san_names);
|
||||
|
||||
for (int i = 0; i < num_names; i++) {
|
||||
GENERAL_NAME *name = sk_GENERAL_NAME_value(san_names, i);
|
||||
|
||||
if (name->type == GEN_URI) {
|
||||
ASN1_STRING *uri_asn1 = name->d.uniformResourceIdentifier;
|
||||
const unsigned char *uri_data = ASN1_STRING_get0_data(uri_asn1);
|
||||
int uri_len = ASN1_STRING_length(uri_asn1);
|
||||
|
||||
if (!uri_data || uri_len <= 0 || memchr(uri_data, '\0', uri_len)) {
|
||||
serverLog(LL_DEBUG, "TLS: Invalid or malformed SAN URI in certificate");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cert_username) {
|
||||
sdsfree(*cert_username);
|
||||
*cert_username = sdsnewlen(uri_data, uri_len);
|
||||
}
|
||||
|
||||
user *u = ACLGetUserByName((const char *)uri_data, uri_len);
|
||||
if (u && (u->flags & USER_FLAG_ENABLED)) {
|
||||
result = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GENERAL_NAMES_free(san_names);
|
||||
return result;
|
||||
}
|
||||
|
||||
user *tlsGetPeerUser(connection *conn_, sds *cert_username) {
|
||||
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:
|
||||
long verify_result = SSL_get_verify_result(conn->ssl);
|
||||
if (verify_result != X509_V_OK) {
|
||||
serverLog(LL_DEBUG, "TLS: Client certificate verification failed: %s",
|
||||
X509_verify_cert_error_string(verify_result));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!field) return NULL;
|
||||
|
||||
X509 *cert = SSL_get_peer_certificate(conn->ssl);
|
||||
X509 *cert = SSL_get0_peer_certificate(conn->ssl);
|
||||
if (!cert) return NULL;
|
||||
|
||||
char field_value[256];
|
||||
sds result = NULL;
|
||||
user *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);
|
||||
switch (server.tls_ctx_config.client_auth_user) {
|
||||
case TLS_CLIENT_FIELD_URI:
|
||||
result = getValidUserFromCertSanUri(cert, cert_username);
|
||||
if (!result) {
|
||||
serverLog(LL_VERBOSE, "TLS: No matching user found in certificate SAN URI fields");
|
||||
}
|
||||
break;
|
||||
|
||||
case TLS_CLIENT_FIELD_CN: {
|
||||
char field_value[256];
|
||||
if (getCertSubjectFieldByName(cert, "CN", field_value, sizeof(field_value))) {
|
||||
if (cert_username) *cert_username = sdsnew(field_value);
|
||||
result = ACLGetUserByName(field_value, strlen(field_value));
|
||||
if (!result || !(result->flags & USER_FLAG_ENABLED)) {
|
||||
serverLog(LL_VERBOSE, "TLS: No matching user found for certificate CN '%s'", field_value);
|
||||
result = NULL;
|
||||
}
|
||||
} else {
|
||||
serverLog(LL_DEBUG, "TLS: Failed to extract CN in certificate subject");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
X509_free(cert);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1860,7 +1917,7 @@ static ConnectionType CT_TLS = {
|
||||
|
||||
/* TLS specified methods */
|
||||
.get_peer_cert = connTLSGetPeerCert,
|
||||
.get_peer_username = tlsGetPeerUsername,
|
||||
.get_peer_user = tlsGetPeerUser,
|
||||
|
||||
/* Miscellaneous */
|
||||
.connIntegrityChecked = connTLSIsIntegrityChecked,
|
||||
|
||||
@@ -174,6 +174,70 @@ start_server {tags {"tls"}} {
|
||||
$s close
|
||||
}
|
||||
|
||||
test {TLS: Auto-authenticate using tls-auth-clients-user (URI)} {
|
||||
# Enable the feature to auto-authenticate based on URI
|
||||
r CONFIG SET tls-auth-clients-user URI
|
||||
|
||||
# Create users matching the URI in the client certificate
|
||||
r ACL SETUSER {urn:valkey:user:first} on >clientpass allcommands
|
||||
r ACL SETUSER {urn:valkey:user:second} on >clientpass allcommands
|
||||
|
||||
# With feature on, client should be auto-authenticated using the URI from SAN
|
||||
# Verify that the authenticated user matches the first URI
|
||||
set s [valkey_client]
|
||||
assert_equal "urn:valkey:user:first" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Turn off the first user
|
||||
r ACL SETUSER {urn:valkey:user:first} off
|
||||
|
||||
# Verify that the authenticated user matches the second URI
|
||||
set s [valkey_client]
|
||||
assert_equal "urn:valkey:user:second" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Turn off the second user
|
||||
r ACL SETUSER {urn:valkey:user:second} off
|
||||
|
||||
# Verify that the authenticated user matches the default
|
||||
set s [valkey_client]
|
||||
assert_equal "default" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Delete all users
|
||||
r ACL DELUSER {urn:valkey:user:first} {urn:valkey:user:second}
|
||||
|
||||
# Verify that the authenticated user matches the default
|
||||
set s [valkey_client]
|
||||
assert_equal "default" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Restore
|
||||
r CONFIG SET tls-auth-clients-user off
|
||||
}
|
||||
|
||||
test {TLS: Verify CN and URI modes are mutually exclusive} {
|
||||
# Create both CN and URI users
|
||||
r ACL SETUSER {Client-only} on >clientpass allcommands allkeys
|
||||
r ACL SETUSER {urn:valkey:user:first} on >clientpass allcommands allkeys
|
||||
|
||||
# Set to CN mode
|
||||
r CONFIG SET tls-auth-clients-user CN
|
||||
set s [valkey_client]
|
||||
assert_equal "Client-only" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Set to URI mode
|
||||
r CONFIG SET tls-auth-clients-user URI
|
||||
set s [valkey_client]
|
||||
assert_equal "urn:valkey:user:first" [$s ACL WHOAMI]
|
||||
$s close
|
||||
|
||||
# Clean up
|
||||
r ACL DELUSER {Client-only} {urn:valkey:user:first}
|
||||
r CONFIG SET tls-auth-clients-user off
|
||||
}
|
||||
|
||||
test {TLS: Auto-reload detects changes} {
|
||||
if {$::tls_module} {
|
||||
# Auto-reload requires built-in TLS
|
||||
|
||||
@@ -56,6 +56,7 @@ nsCertType = server
|
||||
[ client_cert ]
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
nsCertType = client
|
||||
subjectAltName = URI:urn:valkey:user:first, URI:urn:valkey:user:second
|
||||
_END_
|
||||
|
||||
generate_cert server "Server-only" "-extfile tests/tls/openssl.cnf -extensions server_cert"
|
||||
|
||||
+18
-9
@@ -256,18 +256,27 @@ tcp-keepalive 300
|
||||
# tls-auth-clients optional
|
||||
|
||||
# Automatically authenticate TLS clients as Valkey users based on their
|
||||
# certificates.
|
||||
# certificate field values.
|
||||
#
|
||||
# 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.
|
||||
# If set to a field like "CN" or "URI", the server will extract the corresponding
|
||||
# value from the client's TLS certificate and attempt to find a matching 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.
|
||||
# Notes on options:
|
||||
# CN - Extract the Common Name from the certificate's Subject field (single-valued).
|
||||
# URI - Extract the URIs from the certificate's Subject Alternative Name extension.
|
||||
# If multiple URI entries exist, the first URI that matches an enabled
|
||||
# Valkey user is used for authentication.
|
||||
# off - Disable automatic authentication from certificate fields.
|
||||
#
|
||||
# tls-auth-clients-user CN
|
||||
# Default value: off.
|
||||
#
|
||||
# Example:
|
||||
# tls-auth-clients-user URI
|
||||
|
||||
# By default, a replica does not attempt to establish a TLS connection
|
||||
# with its primary.
|
||||
|
||||
Reference in New Issue
Block a user