HKUDS/Vibe-Trading · critical · ValueError

ssl_certfile and ssl_keyfile must both be set for WSS, or bo

Error message

ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty

What it means

Raised by WebSocketChannel._build_ssl_context (called from start) when exactly one of ssl_certfile / ssl_keyfile is set. A TLS server needs a certificate and its matching private key as a pair; providing only one makes WSS impossible, so the channel refuses to start rather than falling back to plaintext.

Source

Thrown at agent/src/channels/websocket.py:376

        except ConnectionClosed:
            self._cleanup_connection(connection)
        except Exception as e:
            self.logger.warning("failed to send {} event: {}", event, e)

    @classmethod
    def default_config(cls) -> dict[str, Any]:
        return WebSocketConfig().model_dump(by_alias=True)

    def _expected_path(self) -> str:
        return _normalize_config_path(self.config.path)

    def _build_ssl_context(self) -> ssl.SSLContext | None:
        cert = self.config.ssl_certfile.strip()
        key = self.config.ssl_keyfile.strip()
        if not cert and not key:
            return None
        if not cert or not key:
            raise ValueError(
                "ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty"
            )
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.minimum_version = ssl.TLSVersion.TLSv1_2
        ctx.load_cert_chain(certfile=cert, keyfile=key)
        return ctx

    # -- HTTP dispatch ------------------------------------------------------

    async def _dispatch_http(self, connection: Any, request: WsRequest) -> Any:
        """Route an inbound HTTP request to the HTTP handler or WS upgrade."""
        got, query = _parse_request_path(request.path)

        # WebSocket upgrade — channel handles this itself
        expected_ws = self._expected_path()
        if got == expected_ws and _is_websocket_upgrade(request):
            client_id = _query_first(query, "client_id") or ""
            if len(client_id) > 128:

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Set both fields: ssl_certfile=/etc/ssl/app/fullchain.pem and ssl_keyfile=/etc/ssl/app/privkey.pem
  2. Check that each path actually exists and is readable by the service user (a wrong path that yields an empty string triggers the same error)
  3. If you terminate TLS at a reverse proxy (nginx/traefik), clear both fields and bind plaintext behind the proxy
  4. Reload/renew certificates if a renewal job emptied one of the files

Example fix

# before
ssl_certfile = "/etc/letsencrypt/live/app/fullchain.pem"
# keyfile missing
# after
ssl_certfile = "/etc/letsencrypt/live/app/fullchain.pem"
ssl_keyfile = "/etc/letsencrypt/live/app/privkey.pem"
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def ssl_pair_ok(cert: str, key: str) -> bool:
    cert, key = cert.strip(), key.strip()
    return (cert and key and Path(cert).is_file() and Path(key).is_file()) or (not cert and not key)

assert ssl_pair_ok(cfg.ssl_certfile, cfg.ssl_keyfile), "provide BOTH cert and key, or neither"

Type guard

def has_complete_ssl_pair(cert: str, key: str) -> bool:
    return bool(cert.strip()) == bool(key.strip())

Try / catch

try:
    await channel.start()
except ValueError as e:
    if "ssl_certfile" in str(e):
        log.error("TLS misconfigured: set both ssl_certfile and ssl_keyfile, or clear both behind a TLS proxy")
    raise

Prevention

When it happens

Trigger: Starting the channel with ssl_certfile set but ssl_keyfile empty (or vice versa). Both fields are stripped; if exactly one is truthy, ValueError is raised before SSLContext is built.

Common situations: Pointing both fields at the same PEM bundle containing only the cert (common with fullchain.pem from Let's Encrypt) and forgetting privkey.pem; env var for one file misnamed so it resolves empty; cert issued as cert+key in separate secret mounts where only one was mounted; migrating from HTTP to WSS and configuring only the certificate.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/c23d35741b127cd8. Report an issue: GitHub.