{"record":{"id":"c23d35741b127cd8","repo":"HKUDS/Vibe-Trading","slug":"ssl-certfile-and-ssl-keyfile-must-both-be-set-for","errorCode":null,"errorMessage":"ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty","messagePattern":"ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"agent/src/channels/websocket.py","lineNumber":376,"sourceCode":"        except ConnectionClosed:\n            self._cleanup_connection(connection)\n        except Exception as e:\n            self.logger.warning(\"failed to send {} event: {}\", event, e)\n\n    @classmethod\n    def default_config(cls) -> dict[str, Any]:\n        return WebSocketConfig().model_dump(by_alias=True)\n\n    def _expected_path(self) -> str:\n        return _normalize_config_path(self.config.path)\n\n    def _build_ssl_context(self) -> ssl.SSLContext | None:\n        cert = self.config.ssl_certfile.strip()\n        key = self.config.ssl_keyfile.strip()\n        if not cert and not key:\n            return None\n        if not cert or not key:\n            raise ValueError(\n                \"ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty\"\n            )\n        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n        ctx.minimum_version = ssl.TLSVersion.TLSv1_2\n        ctx.load_cert_chain(certfile=cert, keyfile=key)\n        return ctx\n\n    # -- HTTP dispatch ------------------------------------------------------\n\n    async def _dispatch_http(self, connection: Any, request: WsRequest) -> Any:\n        \"\"\"Route an inbound HTTP request to the HTTP handler or WS upgrade.\"\"\"\n        got, query = _parse_request_path(request.path)\n\n        # WebSocket upgrade — channel handles this itself\n        expected_ws = self._expected_path()\n        if got == expected_ws and _is_websocket_upgrade(request):\n            client_id = _query_first(query, \"client_id\") or \"\"\n            if len(client_id) > 128:","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/channels/websocket.py#L358-L394","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set both fields: ssl_certfile=/etc/ssl/app/fullchain.pem and ssl_keyfile=/etc/ssl/app/privkey.pem","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)","If you terminate TLS at a reverse proxy (nginx/traefik), clear both fields and bind plaintext behind the proxy","Reload/renew certificates if a renewal job emptied one of the files"],"exampleFix":"# before\nssl_certfile = \"/etc/letsencrypt/live/app/fullchain.pem\"\n# keyfile missing\n# after\nssl_certfile = \"/etc/letsencrypt/live/app/fullchain.pem\"\nssl_keyfile = \"/etc/letsencrypt/live/app/privkey.pem\"","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef ssl_pair_ok(cert: str, key: str) -> bool:\n    cert, key = cert.strip(), key.strip()\n    return (cert and key and Path(cert).is_file() and Path(key).is_file()) or (not cert and not key)\n\nassert ssl_pair_ok(cfg.ssl_certfile, cfg.ssl_keyfile), \"provide BOTH cert and key, or neither\"","typeGuard":"def has_complete_ssl_pair(cert: str, key: str) -> bool:\n    return bool(cert.strip()) == bool(key.strip())","tryCatchPattern":"try:\n    await channel.start()\nexcept ValueError as e:\n    if \"ssl_certfile\" in str(e):\n        log.error(\"TLS misconfigured: set both ssl_certfile and ssl_keyfile, or clear both behind a TLS proxy\")\n    raise","preventionTips":["Reference cert and key from the same issuance dir (letsencrypt live/<domain>/{fullchain,privkey}.pem)","Add a startup preflight that checks both files exist and are readable","If TLS terminates at a proxy, leave both fields empty rather than setting one"],"tags":["websocket","tls","ssl","certificates","config-validation"],"backgroundTag":"ssl-cert-key-mismatch","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}