Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -3428,6 +3428,7 @@ standardConfig static_configs[] = {
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),
createLongLongConfig("tls-client-cert-expiry-warn-threshold", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.tls_client_cert_expiry_warn_threshold, 0, INTEGER_CONFIG, NULL, NULL),
createStringConfig("tls-cert-file", NULL, VOLATILE_CONFIG | MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.cert_file, NULL, 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),
Expand Down
12 changes: 12 additions & 0 deletions src/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,15 @@ sds getListensInfoString(sds info) {

return info;
}

/* Return a printable fingerprint for the peer certificate, when available. */
sds connGetPeerCertFingerprint(connection *conn) {
if (!conn || !conn->type->get_peer_cert_fingerprint) return NULL;
return conn->type->get_peer_cert_fingerprint(conn);
}

/* Return seconds until the peer certificate expires, or C_ERR on failure. */
int connGetPeerCertValidity(connection *conn, long long *remaining_seconds) {
if (!conn || !remaining_seconds || !conn->type->get_peer_cert_validity) return C_ERR;
return conn->type->get_peer_cert_validity(conn, remaining_seconds);
}
7 changes: 7 additions & 0 deletions src/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ typedef struct ConnectionType {

/* TLS specified methods */
sds (*get_peer_cert)(struct connection *conn);
sds (*get_peer_cert_fingerprint)(struct connection *conn);
int (*get_peer_cert_validity)(struct connection *conn, long long *remaining_seconds);

/* Get peer username based on connection type */
sds (*get_peer_username)(connection *conn);
Expand Down Expand Up @@ -424,6 +426,11 @@ static inline sds connGetPeerCert(connection *conn) {
return NULL;
}

/* Return a readable fingerprint (hash) for the peer certificate when supported. */
sds connGetPeerCertFingerprint(connection *conn);
/* Report how many seconds remain before the peer certificate expires. */
int connGetPeerCertValidity(connection *conn, long long *remaining_seconds);

/* Get Peer username based on connection type */
static inline sds connGetPeerUsername(connection *conn) {
if (conn->type && conn->type->get_peer_username) {
Expand Down
72 changes: 72 additions & 0 deletions src/networking.c
Original file line number Diff line number Diff line change
Expand Up @@ -1686,6 +1686,76 @@ int clientHasPendingReplies(client *c) {
}
}

#define TLS_CERT_WARN_DEDUP_WINDOW_MS (24LL * 60 * 60 * 1000)

/* Convert raw seconds to whole days. */
static long long secondsToDaysFloor(long long seconds) {
long long days = seconds / 86400LL;
if (seconds < 0 && (seconds % 86400LL)) days -= 1;
return days;
}

/* Tell us if we already warned about this fingerprint and the suppression window hasn't elapsed. */
static int certWarningRecentlyLogged(sds fingerprint, mstime_t now) {
if (!fingerprint || !server.client_cert_expiry_warned) return 0;
dictEntry *de = dictFind(server.client_cert_expiry_warned, fingerprint);
if (!de) return 0;
long long expiry = *(long long *)dictGetVal(de);
if (expiry > now) return 1;
dictDelete(server.client_cert_expiry_warned, fingerprint);
return 0;
}

/* Remember that we warned about this fingerprint so we can suppress repeats. */
static void rememberCertWarning(sds fingerprint, mstime_t now) {
if (!fingerprint || !server.client_cert_expiry_warned) return;
long long *expiry = zmalloc(sizeof(long long));
*expiry = now + TLS_CERT_WARN_DEDUP_WINDOW_MS;
if (dictAdd(server.client_cert_expiry_warned, fingerprint, expiry) != DICT_OK) {
zfree(expiry);
sdsfree(fingerprint);
}
}

/* Inspect each TLS client certificate as it connects and warn when close to expiry. */
static void monitorTlsClientCertExpiry(client *c) {
if (!connIsTLS(c->conn)) return;

long long remaining_seconds;
if (connGetPeerCertValidity(c->conn, &remaining_seconds) != C_OK) return;

long long remaining_days = secondsToDaysFloor(remaining_seconds);
if (server.client_cert_min_days_until_expiry == -1 || remaining_days < server.client_cert_min_days_until_expiry) {
server.client_cert_min_days_until_expiry = remaining_days;
}

long long threshold_days = server.tls_client_cert_expiry_warn_threshold;
if (threshold_days <= 0) return;

sds fingerprint = connGetPeerCertFingerprint(c->conn);
mstime_t now = mstime();
if (certWarningRecentlyLogged(fingerprint, now)) {
if (fingerprint) sdsfree(fingerprint);
return;
}

long long threshold_seconds = threshold_days * 86400LL;
if (remaining_seconds <= threshold_seconds) {
long long log_days = secondsToDaysFloor(remaining_seconds);
sds client = catClientInfoShortString(sdsempty(), c, server.hide_user_data_from_log);
serverLog(LL_WARNING,
"TLS client certificate for %s expires in %lld days (threshold %lld days).",
client,
log_days,
threshold_days);
sdsfree(client);
rememberCertWarning(fingerprint, now);
fingerprint = NULL;
}

if (fingerprint) sdsfree(fingerprint);
}

void clientAcceptHandler(connection *conn) {
client *c = connGetPrivateData(conn);

Expand All @@ -1696,6 +1766,8 @@ void clientAcceptHandler(connection *conn) {
return;
}

monitorTlsClientCertExpiry(c);

/* If the server is running in protected mode (the default) and there
* is no password set, nor a specific interface is bound, we don't accept
* requests from non loopback interfaces. Instead we try to explain the
Expand Down
17 changes: 16 additions & 1 deletion src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,15 @@ dictType stringSetDictType = {
NULL /* allow to expand */
};

dictType stringLongLongDictType = {
dictSdsHash, /* hash function */
NULL, /* key dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictVanillaFree, /* val destructor */
NULL /* allow to expand */
};

/* Dict for for case-insensitive search using null terminated C strings.
* The key and value do not have a destructor. */
dictType externalStringType = {
Expand Down Expand Up @@ -2268,6 +2277,9 @@ void initServerConfig(void) {
server.pause_cron = 0;
server.dict_resizing = 1;
server.import_mode = 0;
server.tls_client_cert_expiry_warn_threshold = 0;
server.client_cert_min_days_until_expiry = -1;
server.client_cert_expiry_warned = dictCreate(&stringLongLongDictType);

server.latency_tracking_info_percentiles_len = 3;
server.latency_tracking_info_percentiles = zmalloc(sizeof(double) * (server.latency_tracking_info_percentiles_len));
Expand Down Expand Up @@ -2758,6 +2770,8 @@ void resetServerStats(void) {
memset(server.duration_stats, 0, sizeof(durationStats) * EL_DURATION_TYPE_NUM);
server.el_cmd_cnt_max = 0;
lazyfreeResetStats();
if (server.client_cert_expiry_warned) dictEmpty(server.client_cert_expiry_warned, NULL);
server.client_cert_min_days_until_expiry = -1;
}

/* Make the thread killable at any time, so that kill threads functions
Expand Down Expand Up @@ -5959,7 +5973,8 @@ sds genValkeyInfoString(dict *section_dict, int all_sections, int everything) {
"total_blocking_keys_on_nokey:%lu\r\n", blocking_keys_on_nokey,
"paused_reason:%s\r\n", paused_reason,
"paused_actions:%s\r\n", paused_actions,
"paused_timeout_milliseconds:%lld\r\n", paused_timeout));
"paused_timeout_milliseconds:%lld\r\n", paused_timeout,
"client_cert_min_days_until_expiry:%lld\r\n", server.client_cert_min_days_until_expiry));
}

/* Memory */
Expand Down
4 changes: 4 additions & 0 deletions src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -2264,6 +2264,9 @@ struct valkeyServer {
int tls_replication;
int tls_auth_clients;
serverTLSContextConfig tls_ctx_config;
long long tls_client_cert_expiry_warn_threshold; /* Days remaining before logging warnings */
long long client_cert_min_days_until_expiry; /* Minimum days observed across client certificates */
dict *client_cert_expiry_warned; /* Fingerprints logged for expiry warnings */
serverUnixContextConfig unix_ctx_config;
serverRdmaContextConfig rdma_ctx_config;
/* cpu affinity */
Expand Down Expand Up @@ -2710,6 +2713,7 @@ extern double R_Zero, R_PosInf, R_NegInf, R_Nan;
extern hashtableType hashHashtableType;
extern hashtableType hashWithVolatileItemsHashtableType;
extern dictType stringSetDictType;
extern dictType stringLongLongDictType;
extern dictType externalStringType;
extern dictType sdsHashDictType;
extern hashtableType clientHashtableType;
Expand Down
63 changes: 63 additions & 0 deletions src/tls.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#include <openssl/decoder.h>
#endif
Expand Down Expand Up @@ -1216,6 +1217,66 @@ static sds connTLSGetPeerCert(connection *conn_) {
return cert_pem;
}

/* Create a SHA-256 fingerprint string for the peer certificate. */
static sds connTLSGetPeerCertFingerprint(connection *conn_) {
tls_connection *conn = (tls_connection *)conn_;
if ((conn_->type != connectionTypeTls()) || !conn->ssl) return NULL;

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

unsigned char md[EVP_MAX_MD_SIZE];
unsigned int md_len = 0;
sds fingerprint = NULL;

if (X509_digest(cert, EVP_sha256(), md, &md_len)) {
fingerprint = sdsnewlen(NULL, md_len * 2);
static const char hex[] = "0123456789abcdef";
for (unsigned int i = 0; i < md_len; i++) {
fingerprint[2 * i] = hex[(md[i] >> 4) & 0xF];
fingerprint[2 * i + 1] = hex[md[i] & 0xF];
}
}

X509_free(cert);
return fingerprint;
}

static int connTLSGetPeerCertValidity(connection *conn_, long long *remaining_seconds) {
tls_connection *conn = (tls_connection *)conn_;
if (!remaining_seconds || (conn_->type != connectionTypeTls()) || !conn->ssl || !SSL_is_init_finished(conn->ssl))
return C_ERR;

if (SSL_get_verify_result(conn->ssl) != X509_V_OK) return C_ERR;

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

ASN1_TIME *notAfter = X509_get_notAfter(cert);
if (!notAfter) {
X509_free(cert);
return C_ERR;
}

time_t now = server.unixtime ? server.unixtime : time(NULL);
ASN1_TIME *current = ASN1_TIME_set(NULL, now);
if (!current) {
X509_free(cert);
return C_ERR;
}

int days = 0, seconds = 0;
int diff_ok = ASN1_TIME_diff(&days, &seconds, current, notAfter);
ASN1_TIME_free(current);
X509_free(cert);

if (diff_ok != 1) return C_ERR;

long long total_seconds = (long long)days * 86400LL + seconds;
*remaining_seconds = total_seconds;
return C_OK;
}

static ConnectionType CT_TLS = {
/* connection type */
.get_type = connTLSGetType,
Expand Down Expand Up @@ -1263,6 +1324,8 @@ static ConnectionType CT_TLS = {

/* TLS specified methods */
.get_peer_cert = connTLSGetPeerCert,
.get_peer_cert_fingerprint = connTLSGetPeerCertFingerprint,
.get_peer_cert_validity = connTLSGetPeerCertValidity,
.get_peer_username = tlsGetPeerUsername,

/* Miscellaneous */
Expand Down
6 changes: 6 additions & 0 deletions valkey.conf
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ tcp-keepalive 300
#
# tls-session-cache-timeout 60

# Emit a warning log when a client-presented TLS certificate gets close to its
# expiration date. The value is expressed in days. Set to 0 to disable the
# warning; no connections are rejected when the threshold is crossed.
#
# tls-client-cert-expiry-warn-threshold 10

Comment on lines +327 to +332
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very clear why this should be the responsibility of the server to emit these types of events. Can clients not also track this information?

Today, LL_WARNING is reserved for events which need immediate server operator intervention, in this cause it's not the server that has an issue but the end client.

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

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