]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: ssl: Add a way to rate-limit TLSv1.3 KeyUpdate
authorOlivier Houchard <ohouchard@haproxy.com>
Thu, 23 Jul 2026 13:31:18 +0000 (15:31 +0200)
committerOlivier Houchard <cognet@ci0.org>
Thu, 23 Jul 2026 22:31:37 +0000 (00:31 +0200)
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.

include/haproxy/connection-t.h
include/haproxy/ssl_sock-t.h
src/cfgparse-ssl.c
src/connection.c
src/ssl_sock.c

index 891d278f9b1e98456c48d6a8c1608dcbb484c8ec..8ee67faef559bda47ecb2bb60a26a537964600e2 100644 (file)
@@ -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 */
index 70e8010abe679e94dea6f2048e99f9015197288e..b54b5b0ccf2f10bf9a998e658cb4120a798f7b40 100644 (file)
@@ -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 */
index 0c57d6b3303d89e877394b144471ba6cf93122cb..9bf23cf4db7a0a53ae63ad4cb167bcc5aa2c5f87 100644 (file)
@@ -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 },
index 6f310b935f8b0e5987089a9e278ff61764ddebdc..bda89206636c4955832464e27ccd606b283aa1cd 100644 (file)
@@ -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";
index 1bce0f6c5ac40c85868493810c1b9e89c370b10b..bba0e8bdbcc8aee8c42c813cf82c7176c3275388 100644 (file)
@@ -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