fix(api): reap never-readable conns, bound total header time, harden watchdog

Review round 1 on 0ce144d (FIX-FIRST):

F1: stock gthread never reaps a connection that never sends a byte; it
holds a worker_connections slot forever and at 1000 per worker the run
loop stops polling, wedging the worker permanently (survives the
attacker disconnecting). accept/on_client_socket_readable/
murder_keepalived now track such conns in a FIFO with a deadline
(HAPROXY_MGR_FIRST_BYTE_TIMEOUT, 10s) and close expired ones on every
loop pass, including the at-capacity branch.

F2: slowloris (1 header byte per <20s) never tripped the per-recv
timeout. A total request-line+headers deadline
(HAPROXY_MGR_HEADER_DEADLINE, 10s) is armed in handle() and cleared in
handle_request(); a 1s monitor loop shutdown(SHUT_RDWR)s expired
sockets. Armed per thread, not at accept, so legit requests queued
behind busy threads are never cut for waiting.

F4: monitor loop wrapped in try/except with logging; wedge threshold
1200s -> 3600s (multi-domain cert requests run 180s per domain).
F5: docstring now says the timeout bounds each whole sendall().
F6: warn when gunicorn != 23.0.0 (worker overrides gthread internals).
API key compared with hmac.compare_digest.

Tests: +3 production-shape (2x4) tests: 2050 idle conns reaped and
/health recovers while still held; 8 drip conns cut within the deadline
with /health responsive; slow-but-complete headers still served.
15 tests, in the Docker build gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-09 13:58:28 -07:00
1 parent 0ce144d2fb
commit fd06b30d78
5 files changed
+343 -53

No files matched your search

+7 -1
View File
@@ -7,6 +7,7 @@ import jinja2
import socket
import psutil
import functools
import hmac
import logging
from datetime import datetime, timedelta
import json
@@ -165,7 +166,12 @@ def require_api_key(f):
def decorated_function(*args, **kwargs):
if API_KEY:
auth_header = request.headers.get('Authorization')
if not auth_header or auth_header != f'Bearer {API_KEY}':
# Constant-time comparison so response timing doesn't leak how
# much of a guessed key matched. Compared as bytes because
# compare_digest rejects non-ASCII str.
if not auth_header or not hmac.compare_digest(
auth_header.encode('utf-8', 'surrogateescape'),
f'Bearer {API_KEY}'.encode('utf-8', 'surrogateescape')):
return jsonify({'error': 'Unauthorized - Invalid or missing API key'}), 401
return f(*args, **kwargs)
return decorated_function