# Codex 报错 self signed certificate in certificate chain：别关校验

> 公司代理用自己的根证书重签了连接，Codex 里报错的进程不认识它。先分清是客户端、桌面应用、MCP 服务还是沙箱命令报错，再把根证书交给对应的设置，不要关校验。

- Source: https://www.aifreeapi.com/zh/posts/codex-self-signed-certificate-in-certificate-chain
- Language: zh
- Published: 2026-10-02
- Updated: 2026-10-02
- Publisher: AI Free API (https://www.aifreeapi.com)

Codex 报 `self signed certificate in certificate chain`，说的不是你自己签了什么证书。它的意思是：服务器交出来的证书链，最顶上那张根证书不在报错进程的信任库里。在公司电脑或公司网络上，这张根证书多半属于公司的 HTTPS 流量检查代理（也叫 TLS 解密代理）或安全软件，它们会用公司自己的根证书把连接重新签发一遍。

解决办法是把这张根证书交给报错的那个进程，而不是关掉证书校验。如果报错出现在 `codex login` 或 Codex CLI 的请求里，向 IT 拿到公司根证书的 PEM 文件后这样设置：

```bash
export CODEX_CA_CERTIFICATE="$HOME/certs/corp-root.pem"
codex login
```

[Codex 的认证文档](https://learn.chatgpt.com/docs/auth)写明，网络里有公司 TLS 代理或私有根 CA 时，登录前把 `CODEX_CA_CERTIFICATE` 指向 PEM 文件；没设这个变量时 Codex 会改读 `SSL_CERT_FILE`。

如果设了仍然报错，或者报错出现在桌面应用、MCP 服务、Codex 替你执行的 `curl` 或 `npm` 里，原因通常是报错的进程不是 Codex 的客户端：这几类进程各读各的信任设置，一个变量管不了全部。先按下面的对照找到自己那一行。

## 先看是哪个进程报的错：四种现象对应四套信任设置

| 你看到的现象 | 报错的进程 | 它读的信任设置 | 该做什么 |
| --- | --- | --- | --- |
| `codex login` 失败，或 CLI 发请求、连 WebSocket 时报错 | Codex 自己的客户端 | `CODEX_CA_CERTIFICATE`，没有就读 `SSL_CERT_FILE`，再没有只用系统根证书 | 设置 `CODEX_CA_CERTIFICATE` 后重新登录 |
| 桌面应用的 Remote Control 连不上，日志里有 `SELF_SIGNED_CERT_IN_CHAIN` | 桌面应用里的 Node 层 | Node 的根证书：`NODE_USE_SYSTEM_CA=1`、`NODE_EXTRA_CA_CERTS` | 有用户报告带 `NODE_USE_SYSTEM_CA=1` 启动后恢复 |
| 某个 MCP 服务连接或调用失败 | HTTP 型是 Codex 的 MCP 客户端；stdio 型是服务自己的进程 | HTTP 型同第一行；Node 写的 stdio 服务读 `NODE_EXTRA_CA_CERTS` | 在 `config.toml` 里把变量传给这个服务 |
| Codex 执行的命令报 `curl: (60) SSL certificate problem: self signed certificate in certificate chain` | 沙箱里的那个命令 | 工具自己的 CA 设置，例如 curl 的 `CURL_CA_BUNDLE` | 给命令一个显式的 CA 文件，并确认变量传进了沙箱 |

分不清是哪一行时，看报错出现的位置：登录阶段或界面顶部的连接错误属于第一行；带 `[remote-control-transport]` 的日志属于第二行；出现在某个工具调用结果里、前面带着 `curl:` 或 `npm` 字样的，属于第三、四行。

## self signed 是什么意思：链顶的根证书不在信任库里

`self signed certificate in certificate chain` 是 OpenSSL 的校验错误 19：对方给出的证书链完整，但链顶是一张自签名的根证书，而这张根证书不在当前进程的信任库里。所有根证书都是自签名的，区别只在于你的信任库里有没有它。

公司网络上的常见成因是 HTTPS 流量检查：代理在中间把连接解开，再用公司的根证书重新签一张证书给你。技术上它相当于中间人，但这通常是公司明确部署的安全策略。浏览器不报错，是因为 IT 已经把公司根证书装进了系统信任库；Codex 的某个进程报错，是因为那个进程没有读系统信任库，或者读不到。

![公司代理用公司根证书重签连接后，读系统信任库的浏览器和系统 curl 校验通过，没读或读不到系统信任库的 Codex 进程报 OpenSSL 错误 19](https://www.aifreeapi.com/posts/zh/codex-self-signed-certificate-in-certificate-chain/img/trust-store-difference.webp)

另外几种成因也会得到同一句话：杀毒软件的 HTTPS 扫描、用私有 CA 签发证书的自建网关或 MCP 服务，以及指向内部地址的自定义 base URL。

报错如果是 `self-signed certificate`（错误 18，没有 "in certificate chain"），情况不同：那是服务器证书本身自签名，常见于内部测试服务，要信任的是那张服务器证书或它所属的私有 CA。

## 用 openssl s_client 确认是谁签的证书，并拿到根证书 PEM

先确认连接确实被重签，以及是被谁重签的。把主机名换成报错或日志里出现的那个：

```bash
openssl s_client -connect chatgpt.com:443 -showcerts </dev/null
```

看输出里最后一张证书的 `i:`（issuer，签发者）一行，以及末尾的 `Verify return code`：

- 签发者是公司名或安全产品名：连接被公司代理重签，需要的就是这张根证书。
- 签发者是公开的证书机构，返回码是 `0 (ok)`：这条连接没问题，出错的是另一个主机名，或另一个读不到系统信任库的进程。
- 签发者既不是公司，也不是你认识的安全产品：停下来，把输出发给 IT，不要去信任一张来历不明的根证书。

![用 openssl s_client 看签发者后的三种结果：签发者是公司就去拿根证书 PEM，公开 CA 且返回 0 (ok) 就换主机名或查别的进程，签发者不认识就停下来找 IT](https://www.aifreeapi.com/posts/zh/codex-self-signed-certificate-in-certificate-chain/img/issuer-check-flow.webp)

[`openssl s_client`](https://docs.openssl.org/master/man1/openssl-s_client/) 只负责展示证书链，拿根证书有三条路：

1. 向 IT 要 PEM 格式的公司根证书，这是最稳妥的来源。
2. macOS：打开"钥匙串访问"，在"系统"钥匙串里找到公司根证书，用"文件 → 导出项目"存成 `.pem`。
3. Windows：运行 `certmgr.msc`，在"受信任的根证书颁发机构"里找到它，导出时选"Base-64 编码 X.509 (.CER)"，这种格式就是 PEM。

文件用文本编辑器打开，应当以 `-----BEGIN CERTIFICATE-----` 开头。放进 Codex 之前先单独验证它确实能解决问题：

```bash
openssl s_client -connect chatgpt.com:443 -CAfile "$HOME/certs/corp-root.pem" </dev/null 2>/dev/null | grep "Verify return code"
```

输出 `Verify return code: 0 (ok)` 说明证书对了；仍是 `19`，说明拿到的不是链顶那张，或者代理用的是另一张根证书。

## Codex CLI 登录和请求报错：设置 CODEX_CA_CERTIFICATE

Codex 自己的客户端按固定顺序找证书：先读 `CODEX_CA_CERTIFICATE`，没有就读 `SSL_CERT_FILE`，都没有就只用系统根证书。自定义证书是和系统根证书一起加载的，所以 PEM 文件里只放公司根证书就够，不必自己拼一份完整的证书包。

这套设置不只管登录。按 [openai/codex 的 PR #14239](https://github.com/openai/codex/pull/14239)（2026 年 3 月 13 日合并）的说明，Codex 向外发起的 HTTPS 请求和安全 WebSocket 连接都读同一组变量，包括后端请求、云端任务、MCP 的 HTTP 客户端和实时 WebSocket。很旧的版本只在登录时读自定义证书或完全不读，这种情况先升级 Codex 再排查。

要让变量每次都生效，把它写进 shell 配置文件：

```bash
# macOS / Linux：写入 ~/.zshrc 或 ~/.bashrc
export CODEX_CA_CERTIFICATE="$HOME/certs/corp-root.pem"
```

```powershell
# Windows PowerShell：写入用户环境变量，重开终端后生效
setx CODEX_CA_CERTIFICATE "C:\certs\corp-root.pem"
```

有三点会让设置看起来"没生效"：

- 变量必须出现在启动 Codex 的那个进程的环境里。从 Dock 或开始菜单启动的 IDE 不一定读你的 shell 配置文件；IDE 里的 Codex 仍报错时，从已经设好变量的终端启动 IDE 再试。
- 路径写错或文件不是 PEM 时，Codex 的报错会直接点出变量名和路径，按提示改。
- 你的环境里如果早就有一个指向别处的 `SSL_CERT_FILE`，而 `CODEX_CA_CERTIFICATE` 没设，Codex 读的就是那个文件。

设备码登录（`codex login --device-auth`）帮不上忙：它省掉的是本机浏览器回调，登录请求照样走 HTTPS，照样要过证书校验。

## 桌面应用 Remote Control 连不上：试 NODE_USE_SYSTEM_CA=1

桌面应用里有一层基于 Node 的代码，Node 默认只信任自己内置的根证书，不看系统钥匙串。所以会出现这种情况：公司根证书已经在系统钥匙串里被信任，curl 和浏览器都正常，桌面应用的 Remote Control 却报 `self signed certificate in certificate chain`，日志里是：

```text
[remote-control-transport] remote_control_websocket.connect_failed_before_open
```

这是 [openai/codex 的 issue #43489](https://github.com/openai/codex/issues/43489) 里一位用户在 2026 年 9 月 7 日报告的现象（应用版本 26.901.51231，macOS 26.6.1）。报告者的做法是带着 `NODE_USE_SYSTEM_CA=1` 从终端启动应用，让 Node 改读系统钥匙串：

```bash
NODE_USE_SYSTEM_CA=1 /Applications/ChatGPT.app/Contents/MacOS/ChatGPT
```

需要知道的边界：这是一位用户的报告，截至 2026 年 10 月 2 日 issue 仍未关闭，也没有维护者确认。它成立的前提是公司根证书已经在系统钥匙串里并被设为信任。`NODE_USE_SYSTEM_CA` 的含义见 [Node.js 的命令行文档](https://nodejs.org/api/cli.html)：让 Node 加载操作系统的证书库（macOS 钥匙串、Windows 证书存储、Linux 上 OpenSSL 的默认位置）。桌面应用的 Node 层是否读 `CODEX_CA_CERTIFICATE`，以及 Windows、Linux 上的桌面应用是否有同样的问题，公开文档和这个 issue 里都没有记载。

这个启动方式只对当次启动有效。它管用的话，把你的版本和现象补充到 issue 里，并让 IT 知道桌面应用需要这样启动。

## MCP 服务报证书错误：HTTP 型跟随 Codex，stdio 型要单独传变量

MCP 服务分两种，报错的进程不同。

通过 URL 连接的 HTTP 型服务，发请求的是 Codex 的 MCP 客户端，它读的就是 `CODEX_CA_CERTIFICATE` / `SSL_CERT_FILE`。公司内部用私有 CA 签发的 MCP 服务也属于这一类：把那个私有 CA 的根证书追加进同一个 PEM 文件即可，一个文件可以放多张证书。

stdio 型服务是 Codex 启动的一个独立进程，访问外部接口的是它自己。用 Node 写的服务（多数用 `npx` 启动的都是）读 `NODE_EXTRA_CA_CERTS`，这个变量把文件里的证书加到 Node 内置的根证书之外。在 `config.toml` 里把它传给对应的服务，写法见 [Codex 的 MCP 文档](https://learn.chatgpt.com/codex/extend/mcp)：

```toml
[mcp_servers.your-server]
command = "npx"
args = ["-y", "your-mcp-package"]

[mcp_servers.your-server.env]
NODE_EXTRA_CA_CERTS = "/Users/you/certs/corp-root.pem"
```

`NODE_EXTRA_CA_CERTS` 有两个容易踩的地方：它只在进程启动时读一次，改完要重启 Codex 让服务重新拉起；文件格式有问题时 Node 只打印一条警告就继续运行，看起来像"设置了但没用"，这时回头检查 PEM 文件本身。

其他语言写的 MCP 服务有各自的 CA 设置，查那个运行时或服务本身的文档。

## 沙箱里的 curl 报 curl: (60)：给命令一个显式的 CA 文件

Codex 在沙箱里执行的命令是另一批进程，`CODEX_CA_CERTIFICATE` 管不到它们。在 macOS 上还多一层原因：按 [issue #11182](https://github.com/openai/codex/issues/11182) 报告者的分析（2026 年 2 月 9 日，codex-cli 0.98.0），系统自带的 curl 在 `/etc/ssl/cert.pem` 里找不到根证书时会去问钥匙串的信任服务 `com.apple.TrustEvaluationAgent`，而沙箱拒绝了这次查询。结果是同一条 `curl` 命令在你自己的终端里成功，由 Codex 执行就报：

```text
curl: (60) SSL certificate problem: self signed certificate in certificate chain
```

这个 issue 仍未关闭，维护者没有给出处理办法。能做的是不依赖钥匙串，直接给工具一个 CA 文件，这是 curl 本身的标准用法，issue 里并没有验证它在沙箱中一定奏效：

```bash
# curl 的 CA 文件会取代默认证书包，所以把系统证书包和公司根证书合在一起
cat /etc/ssl/cert.pem "$HOME/certs/corp-root.pem" > "$HOME/certs/corp-bundle.pem"
export CURL_CA_BUNDLE="$HOME/certs/corp-bundle.pem"
```

单次命令也可以写 `curl --cacert "$HOME/certs/corp-bundle.pem" https://…`。由 Codex 执行的 `npm` 和其他 Node 工具同理，用 `NODE_EXTRA_CA_CERTS`。

还要确认变量真的传进了沙箱。`config.toml` 里的 `shell_environment_policy` 决定 Codex 把哪些环境变量交给它启动的命令，你在 shell 里 export 的变量不一定到得了那里。可以用 `set` 显式写入：

```toml
[shell_environment_policy]
set = { CURL_CA_BUNDLE = "/Users/you/certs/corp-bundle.pem", NODE_EXTRA_CA_CERTS = "/Users/you/certs/corp-root.pem" }
```

其余键（继承范围、过滤规则）的取值以 [Codex 的高级配置文档](https://learn.chatgpt.com/docs/config-file/config-advanced)为准；沙箱和批准策略本身怎么配，见 [Codex 沙箱与 config.toml 的权限、批准和网络配置](/zh/posts/codex-config-toml)。

## 为什么不要用 NODE_TLS_REJECT_UNAUTHORIZED=0 关掉校验

因为它没有解决"进程不认识公司根证书"，而是让进程不再检查任何证书。Node.js 文档对 `NODE_TLS_REJECT_UNAUTHORIZED=0` 的说明是：它会关闭证书校验，使 TLS 连接暴露在中间人攻击之下。`npm config set strict-ssl false`、`curl -k`、`--insecure` 是同一类做法。

对 Codex 来说代价很具体：经过这些连接的有登录凭据、访问令牌和你的代码。校验关掉之后，任何能插到你和服务器之间的设备都可以冒充对方，而你看不到任何提示。加一张根证书只是多信任一个你知道来历的签发者，其余校验照常进行；两者不是同一件事的快慢两种做法。

## 验证证书生效，以及什么时候该找 IT

验证只需要重做刚才失败的那一步，并且在同一个环境里做：

- CLI：新开一个终端，`echo $CODEX_CA_CERTIFICATE` 确认变量在，再运行 `codex login` 或重新发一次请求。
- 桌面应用：用带变量的方式启动后重新连接 Remote Control，日志里不再出现 `connect_failed_before_open`。
- MCP 服务：重启 Codex，让服务重新拉起，再调用一次它的工具。
- 沙箱命令：让 Codex 重新执行同一条 `curl`，而不是你自己在终端里跑，两者的环境不同。

出现下面的情况，停下来找 IT，不要继续自己试：

- 拿不到根证书，或者公司规定不允许自行导出。
- `openssl` 显示的签发者不是公司，也不是你认识的安全产品。
- 根证书确认无误，`openssl -CAfile` 也返回 `0 (ok)`，但 Codex 仍然报错。把报错原文、出错的主机名和 `openssl s_client` 的输出一起交给 IT，问题可能在代理对 WebSocket 或特定域名的处理上。
- 设备由公司托管，环境变量或配置文件改不了。

证书问题解决后如果登录仍然失败，按 [Codex 报错 Token Exchange Failed 的分阶段排查](/zh/posts/codex-access-token-could-not-be-refreshed)继续；任务中途反复断线，见 [Codex 一直 Reconnecting 时怎么定位是哪一层断了](/zh/posts/codex-stuck-reconnecting-fix)。

## 根证书已经装进钥匙串，为什么 Codex 还报这个错？

因为装进系统信任库只对读它的进程有效。浏览器和 macOS 自带的 curl 会读钥匙串；Node 默认只用内置根证书，要靠 `NODE_USE_SYSTEM_CA=1` 或 `NODE_EXTRA_CA_CERTS` 才认；沙箱里的命令按用户报告访问不到钥匙串的信任服务。Codex 自己的客户端会加载系统根证书，但设置 `CODEX_CA_CERTIFICATE` 更直接，也不依赖系统信任库在各平台上的差异。

## 登录时报 Token exchange failed，也是证书的问题吗？

有可能。[issue #6849](https://github.com/openai/codex/issues/6849) 里，旧版 CLI（0.58.0）在带自定义 CA 的公司代理后面登录，报的是 `Token exchange failed: error sending request for url (https://auth.openai.com/oauth/token)`，没有出现 "self signed" 字样，根因却相同。用上面的 `openssl s_client` 命令连 `auth.openai.com:443` 看签发者：是公司代理，就升级 Codex 并设置 `CODEX_CA_CERTIFICATE`；签发者是公开证书机构，原因在别处。
