Daytona 自托管 + 自签名证书:一次完整的 SSL 排查实录

自托管 Daytona 时使用自签名证书,Python SDK 的 SSL 验证问题比你想象的要复杂得多。这篇文章记录了从第一个报错到最终解决的完整过程,以及每一步为什么失败、为什么最终能成功。


背景

Daytona 是一个为 AI Agent 设计的沙盒运行时基础设施,支持通过 Python SDK 在隔离环境中执行代码。自托管时如果使用自签名证书(如内网 IP 直接部署),SDK 的 SSL 验证会持续报错。

环境:

  • Daytona 自托管,地址 https://<your-server-ip>:<port>
  • 自签名证书(非 CA 签发)
  • Python SDK:daytona / daytona_sdk

第一个报错:sandbox 创建失败

Failed to create sandbox: HTTPSConnectionPool(host='<your-server-ip>', port=<port>):
Max retries exceeded with url: /api/sandbox
(Caused by SSLError(SSLCertVerificationError(1,
'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))

第一次尝试:事后修改 configuration

自然的第一反应——创建 Daytona 实例后,直接修改内部的 configuration:

daytona_client = Daytona(DaytonaConfig(...))
daytona_client._api_client.configuration.ssl_ca_cert = str(cert)

为什么失败? 看 RESTClientObject.__init__ 的源码:

class RESTClientObject:
    def __init__(self, configuration) -> None:
        if configuration.verify_ssl:
            cert_reqs = ssl.CERT_REQUIRED
        else:
            cert_reqs = ssl.CERT_NONE

        pool_args = {
            "cert_reqs": cert_reqs,
            "ca_certs": configuration.ssl_ca_cert,
            # ...
        }
        self.pool_manager = urllib3.PoolManager(**pool_args)

cert_reqs 和 ca_certs 在 __init__ 时就固化进了 urllib3.PoolManager。之后再改 configuration 的任何属性,连接池不会感知,已有连接照旧复用旧的 SSL 配置。


第二个报错:文件上传失败

用 patch Configuration.__init__ 的方式解决了 sandbox 创建,但立刻出现了新问题:

Failed to upload files: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: self-signed certificate (_ssl.c:1010)

sandbox 创建成功了,命令执行也成功了,唯独文件上传失败。

原因:Daytona 有两个 API Client

看 Daytona.__init__ 的源码:

self._api_client: ApiClient = ApiClient(configuration)          # 主 API
self._toolbox_api_client: ToolboxApiClient = self._clone_api_client_to_toolbox_api_client()  # toolbox

toolbox 是单独 clone 出来的,文件上传、文件读写等操作走的是 toolbox client,不是主 API client。

def _clone_api_client_to_toolbox_api_client(self) -> ToolboxApiClient:
    config = deepcopy(self._api_client.configuration)
    config.host = ""   # host 被清空,运行时动态注入
    toolbox_api_client = ToolboxApiClient(config)
    return toolbox_api_client

所以需要同时 patch 两个地方。

第二次尝试:patch ToolboxConfiguration

from daytona_toolbox_api_client import Configuration as ToolboxConfiguration

def _toolbox_patched(self, *args, **kwargs):
    _toolbox_orig(self, *args, **kwargs)
    self.ssl_ca_cert = str(cert)

ToolboxConfiguration.__init__ = _toolbox_patched

依然失败。 诊断发现:

inner client configuration type: <class 'daytona_api_client.configuration.Configuration'>

toolbox client 用的 Configuration 根本不是 daytona_toolbox_api_client.Configuration,而是 daytona_api_client.Configuration(因为 deepcopy 的是主 client 的 configuration)。patch 的根本就不是同一个类。


深入排查:toolbox 请求走哪里

进一步看 Sandbox.__init__:

self._toolbox_api = ToolboxApiClientProxy(
    toolbox_api, self.id, self.toolbox_proxy_url  # ← 来自 API response
)

ToolboxApiClientProxy 在每次请求时动态覆盖 host:

def param_serialize(self, *args, **kwargs):
    kwargs["_host"] = self._toolbox_base_url  # 动态注入 host
    return self._api_client.param_serialize(*args, **kwargs)

打印出来:

toolbox_proxy_url: https://<your-server-ip>:<port>/toolbox

同一台机器,同一张证书,但每次请求动态换 host 会让 urllib3 新建连接池——新连接池的 SSL 配置从 configuration 读,但如果 RESTClientObject 已经建好了,改 configuration 也没用。


关键发现:还有 httpx

verify_ssl = False 的 patch 加上了,configuration.verify_ssl 打印出来也是 False,但还是报错。

这说明某些请求根本不走 urllib3,而是走 httpx。

Daytona SDK 同时使用了两个 HTTP 库:

  • urllib3(通过 openapi-generator 生成的 ApiClient)→ 处理主 API 和 toolbox 的大部分请求
  • httpx → 处理部分操作(文件上传等流式请求)

这就是为什么 patch Configuration 始终无法完全解决问题的根本原因。


最终解决方案

需要同时 patch 两个 HTTP 库:

import httpx
from pathlib import Path
from daytona_api_client import Configuration
from daytona import Daytona, DaytonaConfig

cert = Path(__file__).resolve().parents[2] / "cert.pem"

# 1. Patch httpx.Client — 处理文件上传等操作
_orig_httpx_init = httpx.Client.__init__

def patched_httpx_init(self, *args, **kwargs):
    kwargs.setdefault("verify", str(cert))  # 只在未指定时注入,不覆盖显式配置
    return _orig_httpx_init(self, *args, **kwargs)

httpx.Client.__init__ = patched_httpx_init

# 2. Patch urllib3 的 Configuration — 处理主 API 和 toolbox 的常规请求
_orig_config_init = Configuration.__init__

def patched_config_init(self, *args, **kwargs):
    _orig_config_init(self, *args, **kwargs)
    self.ssl_ca_cert = str(cert)  # 证书路径注入连接池参数

Configuration.__init__ = patched_config_init

# 3. 构造 Daytona 客户端(两个 patch 必须在这之前生效)
daytona_client = Daytona(DaytonaConfig())

# 4. 可选:还原 patch,避免影响后续其他代码
httpx.Client.__init__ = _orig_httpx_init
Configuration.__init__ = _orig_config_init


为什么这样可以

Daytona Python SDK

主 API Client
urllib3

Toolbox API Client
urllib3 deepcopy

文件上传/流式请求
httpx

Configuration.__init__ patch
ssl_ca_cert 注入连接池

httpx.Client.__init__ patch
verify 参数注入

请求类型HTTP 库修复方式
sandbox 创建、命令执行urllib3Configuration.__init__ patch
toolbox 常规操作urllib3(deepcopy)同上,因为 deepcopy 继承了 patch 后的 configuration
文件上传(fs.upload_file)httpxhttpx.Client.__init__ patch

关键细节:

kwargs.setdefault("verify", str(cert)) 用 setdefault 而非直接赋值,只在调用方没有显式传 verify 时才注入。这样不会影响其他代码里有意设置 verify=True 或 verify=False 的 httpx 请求,做到了最小侵入。


证书文件如何获取

如果还没有 cert.pem,从服务器直接导出:

openssl s_client -connect <your-server-ip>:<port> -showcerts </dev/null 2>/dev/null \
  | openssl x509 -outform PEM > cert.pem

# 验证自签名证书(用自身作为 CA 验证)
openssl verify -CAfile cert.pem cert.pem
# 输出 cert.pem: OK 则正确


踩坑总结

  1. 事后改 configuration 无效:urllib3 的 PoolManager 在 RESTClientObject.__init__ 时就把 SSL 参数固化了,之后改 configuration 属性不会影响已有连接池。
  2. patch 要在构造前:Daytona() 构造时会立即建立连接池,必须在此之前完成 patch。
  3. toolbox 用的不是 daytona_toolbox_api_client.Configuration:它是从主 client deepcopy 来的,patch 错了类完全没用。
  4. SDK 混用了 urllib3 和 httpx:只 patch 一个不够,文件上传走 httpx,必须两个都 patch。
  5. 自签名证书 ≠ 私有 CA:ssl_ca_cert 期望的是 CA 证书,自签名证书需要把证书本身当 CA 来信任,导出方式见上。

长期建议

这套 patch 方案能用,但毕竟是 monkey patch,依赖内部实现,SDK 大版本升级可能失效。更稳妥的长期方案:

  • 用 mkcert 生成本地受信任证书:mkcert <your-server-ip>,系统级信任,SDK 无需任何额外配置。
  • 向 Daytona 提 issue:在 DaytonaConfig 加 ssl_ca_cert 和 verify_ssl 字段,从根本上支持自定义证书,改动极小。
  • 内网部署用 Caddy + Let’s Encrypt DNS-01:即使是内网 IP,也可以通过域名 + DNS 验证获取公网受信任证书。
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐