From: Olivier Houchard Date: Thu, 23 Jul 2026 13:31:18 +0000 (+0200) Subject: MEDIUM: ssl: Add a way to rate-limit TLSv1.3 KeyUpdate X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=91004114fe8816f848025fe71def4ea23e72a5f6;p=thirdparty%2Fhaproxy.git MEDIUM: ssl: Add a way to rate-limit TLSv1.3 KeyUpdate Processing TLSv1.3 KeyUpdate is expensive in term of CPU, and in normal usage there is very few reason to get a lot of them. So add a new keyword, tune.ssl.keyupdate-rate-limit, that gives the maximum number of KeyUpdate we're okay with receiving per second. The default is 100, which should be enough. 0 means no rate-limiting at all. This should mitigate the problem reported in Github issue #3450. This should be backported as far back as 2.8. --- diff --git a/include/haproxy/connection-t.h b/include/haproxy/connection-t.h index 891d278f9..8ee67faef 100644 --- a/include/haproxy/connection-t.h +++ b/include/haproxy/connection-t.h @@ -270,6 +270,7 @@ enum { CO_ER_SSL_TOO_MANY, /* too many SSL connections */ CO_ER_SSL_NO_MEM, /* no more memory to allocate an SSL connection */ CO_ER_SSL_RENEG, /* forbidden client renegotiation */ + CO_ER_SSL_KEYUPDATE, /* too many TLS1.3 KeyUpdate messages */ CO_ER_SSL_CA_FAIL, /* client cert verification failed in the CA chain */ CO_ER_SSL_CRT_FAIL, /* client cert verification failed on the certificate */ CO_ER_SSL_MISMATCH, /* Server presented an SSL certificate different from the configured one */ diff --git a/include/haproxy/ssl_sock-t.h b/include/haproxy/ssl_sock-t.h index 70e8010ab..b54b5b0cc 100644 --- a/include/haproxy/ssl_sock-t.h +++ b/include/haproxy/ssl_sock-t.h @@ -257,6 +257,7 @@ struct ssl_keylog { #define SSL_SOCK_F_HAS_ALPN (1 << 5) /* An ALPN has been negotiated */ #define SSL_SOCK_F_KTLS_ULP (1 << 6) /* TLS ULP is enabled on that socket */ #define SSL_SOCK_F_KTLS_RX_CTRL (1 << 7) /* a non-app-data record is blocking kTLS splicing, fall back to a buffered read to consume it */ +#define SSL_SOCK_F_KILL (1 << 9) /* connection must be aborted (e.g. KeyUpdate flood) */ struct ssl_sock_ctx { struct connection *conn; @@ -271,6 +272,8 @@ struct ssl_sock_ctx { struct buffer early_buf; /* buffer to store the early data received */ int sent_early_data; /* Amount of early data we sent so far */ int flags; /* Various flags for the ssl_sock_ctx */ + uint keyupdate_tokens; /* remaining TLS1.3 KeyUpdate token budget */ + uint keyupdate_last_ms; /* last KeyUpdate token refill */ #ifdef HA_USE_KTLS char record_type; /* Record type to use if not just sending application data */ #endif @@ -320,6 +323,7 @@ struct global_ssl { int ctx_cache; /* max number of entries in the ssl_ctx cache. */ int capture_buffer_size; /* Size of the capture buffer. */ int keylog; /* activate keylog */ + uint keyupdate_max; /* max received TLS1.3 KeyUpdates per second per connection (0 == unlimited) */ int extra_files; /* which files not defined in the configuration file are we looking for */ int extra_files_noext; /* whether we remove the extension when looking up a extra file */ int security_level; /* configure the openssl security level */ diff --git a/src/cfgparse-ssl.c b/src/cfgparse-ssl.c index 0c57d6b33..9bf23cf4d 100644 --- a/src/cfgparse-ssl.c +++ b/src/cfgparse-ssl.c @@ -521,6 +521,32 @@ static int ssl_parse_global_keylog(char **args, int section_type, struct proxy * } #endif +/* + * parse "tune.ssl.keyupdate-rate-limit" : max received TLS1.3 KeyUpdate + * messages per second and per connection. 0 disables the limit. + */ +static int ssl_parse_global_keyupdate_ratelimit(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + int val; + + if (too_many_args(1, args, err, NULL)) + return -1; + + if (!*args[1]) { + memprintf(err, "'%s' expects a number (KeyUpdates/s, 0 to disable).", args[0]); + return -1; + } + val = atoi(args[1]); + if (val < 0) { + memprintf(err, "'%s' expects a positive number.", args[0]); + return -1; + } + global_ssl.keyupdate_max = val; + return 0; +} + /* Allow to explicitly disable certificate compression when set to "off" */ #ifdef SSL_OP_NO_RX_CERTIFICATE_COMPRESSION static int ssl_parse_certificate_compression(char **args, int section_type, struct proxy *curpx, @@ -2825,6 +2851,7 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.ssl.capture-cipherlist-size", ssl_parse_global_capture_buffer }, { CFG_GLOBAL, "tune.ssl.capture-buffer-size", ssl_parse_global_capture_buffer }, { CFG_GLOBAL, "tune.ssl.keylog", ssl_parse_global_keylog }, + { CFG_GLOBAL, "tune.ssl.keyupdate-rate-limit", ssl_parse_global_keyupdate_ratelimit }, { CFG_GLOBAL, "ssl-default-bind-ciphers", ssl_parse_global_ciphers }, { CFG_GLOBAL, "ssl-default-server-ciphers", ssl_parse_global_ciphers }, { CFG_GLOBAL, "ssl-default-bind-curves", ssl_parse_global_curves }, diff --git a/src/connection.c b/src/connection.c index 6f310b935..bda892066 100644 --- a/src/connection.c +++ b/src/connection.c @@ -908,6 +908,7 @@ const char *conn_err_code_name(struct connection *c) case CO_ER_SSL_TOO_MANY: return "SSL_TOO_MANY"; case CO_ER_SSL_NO_MEM: return "SSL_NO_MEM"; case CO_ER_SSL_RENEG: return "SSL_RENEG"; + case CO_ER_SSL_KEYUPDATE: return "SSL_KEYUPDATE"; case CO_ER_SSL_CA_FAIL: return "SSL_CA_FAIL"; case CO_ER_SSL_CRT_FAIL: return "SSL_CRT_FAIL"; case CO_ER_SSL_MISMATCH: return "SSL_MISMATCH"; @@ -979,6 +980,7 @@ const char *conn_err_code_str(struct connection *c) case CO_ER_SSL_TOO_MANY: return "Too many SSL connections"; case CO_ER_SSL_NO_MEM: return "Out of memory when initializing an SSL connection"; case CO_ER_SSL_RENEG: return "Rejected a client-initiated SSL renegotiation attempt"; + case CO_ER_SSL_KEYUPDATE: return "Aborted connection after a TLS 1.3 KeyUpdate flood"; case CO_ER_SSL_CA_FAIL: return "SSL client CA chain cannot be verified"; case CO_ER_SSL_CRT_FAIL: return "SSL client certificate not trusted"; case CO_ER_SSL_MISMATCH: return "Server presented an SSL certificate different from the configured one"; diff --git a/src/ssl_sock.c b/src/ssl_sock.c index 1bce0f6c5..bba0e8bdb 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -139,6 +139,7 @@ struct global_ssl global_ssl = { .default_dh_param = SSL_DEFAULT_DH_PARAM, .ctx_cache = DEFAULT_SSL_CTX_CACHE, .capture_buffer_size = 0, + .keyupdate_max = 100, /* max received TLS1.3 KeyUpdates/s per conn; 0 to disable */ .extra_files = SSL_GF_ALL, .extra_files_noext = 0, #ifdef HAVE_SSL_KEYLOG @@ -387,6 +388,15 @@ static int ha_ssl_read(BIO *h, char *buf, int size) int flags; ctx = BIO_get_data(h); + + /* A KeyUpdate flood was detected, we should abort now, as we may + * have plenty more KeyUpdate to deal with, and that would end up + * triggering the watchdog. + */ + if (ctx->flags & SSL_SOCK_F_KILL) { + ctx->conn->flags |= CO_FL_ERROR; + return -1; + } #ifdef HA_USE_KTLS #ifdef HAVE_VANILLA_OPENSSL if (ctx->flags & SSL_SOCK_F_KTLS_RECV) { @@ -869,6 +879,10 @@ static void ssl_init_keylog(int write_p, int version, SSL *ssl); #endif +static void ssl_sock_keyupdate_ratelimit(int write_p, int version, + int content_type, const void *buf, + size_t len, SSL *ssl); + /* List head of all registered SSL/TLS protocol message callbacks. */ struct list ssl_sock_msg_callbacks = LIST_HEAD_INIT(ssl_sock_msg_callbacks); @@ -914,6 +928,10 @@ static int ssl_sock_register_msg_callbacks(void) return ERR_ABORT; } #endif + if (global_ssl.keyupdate_max) { + if (!ssl_sock_register_msg_callback(ssl_sock_keyupdate_ratelimit)) + return ERR_ABORT; + } #ifdef USE_QUIC_OPENSSL_COMPAT if (!ssl_sock_register_msg_callback(quic_tls_compat_msg_callback)) return ERR_ABORT; @@ -2214,6 +2232,49 @@ static void ssl_init_keylog(int write_p, int version, } #endif +static void ssl_sock_keyupdate_ratelimit(int write_p, int version, + int content_type, const void *buf, + size_t len, SSL *ssl) +{ + struct ssl_sock_ctx *ctx = NULL; + struct connection *conn; + uint rate, elapsed; + + /* only count KeyUpdate messages received from the peer */ + if (write_p || content_type != SSL3_RT_HANDSHAKE) + return; + if (len < 1 || ((const unsigned char *)buf)[0] != SSL3_MT_KEY_UPDATE) + return; + + conn = ssl_sock_get_conn(ssl, &ctx); + if (!conn || !ctx) + return; + + rate = global_ssl.keyupdate_max; + elapsed = now_ms - ctx->keyupdate_last_ms; /* wrap-safe */ + if (elapsed) { + uint refill = (uint)((ullong)elapsed * rate / 1000); + + if (refill) { + ctx->keyupdate_tokens += refill; + if (ctx->keyupdate_tokens > rate) + ctx->keyupdate_tokens = rate; + ctx->keyupdate_last_ms = now_ms; + } + } + + if (ctx->keyupdate_tokens) + ctx->keyupdate_tokens--; + else { + /* received way too many KeyUpdates, kill the connection */ + if (conn->owner) + session_add_glitch_ctr(conn->owner, 1); + ctx->flags |= SSL_SOCK_F_KILL; + conn->flags |= CO_FL_ERROR; + conn->err_code = CO_ER_SSL_KEYUPDATE; + } +} + /* Callback is called for ssl protocol analyse */ static __maybe_unused void ssl_sock_msgcbk(int write_p, int version, int content_type, const void *buf, size_t len, SSL *ssl, void *arg) { @@ -5938,6 +5999,8 @@ static int ssl_sock_init(struct connection *conn, void **xprt_ctx) ctx->xprt_ctx = NULL; ctx->error_code = 0; ctx->flags = SSL_SOCK_F_EARLY_ENABLED; + ctx->keyupdate_tokens = global_ssl.keyupdate_max; + ctx->keyupdate_last_ms = now_ms; #ifdef HA_USE_KTLS ctx->record_type = 0; #endif