最近帮一个同事排查问题,场景很典型:SSH 连远程服务器一切正常,密码和密钥都过了,服务器上的服务也跑得没问题。结果他想在这台远程服务器上用 codex 登录,命令敲下去,终端直接甩了一个 403 错误。更让人头大的是,日志里还冒出一句 “cc switch local proxy failed while handling codex endpoint /responses”。当时我们两个对着屏幕看了半天,第一反应是“SSH 权限出问题了?”但仔细一想,SSH 都连上了,跟登录 codex 有什么关系?
这篇文章就把这次排查的完整过程写下来。如果你也在远程服务器上跑 codex,遇到登录 403,或者日志里出现类似 local proxy 的报错,这篇文章应该能帮你省掉不少弯路。我会把每一步的排查思路、命令、背后原理都讲清楚,包括一些文档里不会写的细节。整个排查思路不只适用于 codex,任何通过 SSH 远程操作客户端登录第三方服务时报 403,都可以参考。
1. 先分清:403 到底是 SSH 给的,还是 codex 对端给的
1.1 一个典型的报错现场
先还原一下现场。同事的机器是一台 Linux 服务器,他用 SSH 登录后执行codex login,没过几秒输出类似这样的内容:
Error: 403 Forbidden Provider: codex Endpoint: /responses cc switch local proxy failed while handling codex endpoint /responses. provider...第一眼看到这个报错,很容易把注意力放在 SSH 上,因为标题里同时出现了 SSH、codex、403 三个关键词。但这里有个非常重要的判断:SSH 连接成功,说明 SSH 层的认证是没问题的。403 是 HTTP 状态码,是 codex 客户端发请求之后,服务端(或者中间代理/网关)返回的响应。也就是说,这个 403 基本不可能是 SSH 服务本身给的,除非你连的是一个奇怪的堡垒机,在 Shell 层面给你返回 HTTP 状态码。
我们当时做了一件事:先看 codex 的详细日志。codex 在 Linux 上通常会把日志写到~/.codex/logs/目录下,你可以直接打开最近一个日志文件,或者用tail -f边执行边看:
tail -f ~/.codex/logs/*.log日志里除了上面那句 local proxy 之外,还会记录完整的请求 URL、请求头、响应状态码和响应体。很多时候,真正的错误原因藏在响应体里,而不是状态码本身。比如响应体里写了invalid_api_key,那就是 key 的问题;如果写了ip_not_allowed,那就是出口 IP 被限制;如果写了request_time_too_skewed,那就是服务器时间不对。所以遇到 403,第一件事不是改配置,而是翻日志看响应体。
1.2 403 与 401 的区别,以及响应体的价值
这里要顺手科普一下 401 和 403 的区别,很多人会混。
- 401 Unauthorized:服务器没认出你是谁,常见于没带 token、token 格式错误、token 过期。
- 403 Forbidden:服务器认出你了(或者至少收到了你的请求),但拒绝你执行这个操作。可能是权限不足,也可能是 IP 被拒、地域被拒、组织被禁用、请求被防火墙策略拦截后网关统一回 403。
但在实际排查中,很多服务因为安全考虑会把 401 和 403 混着用,甚至统一返回 403,所以不要死抠状态码语义。关键是响应头里的WWW-Authenticate、响应体里的error字段、以及请求头里的Authorization是否真实传递。
我在那次排查里做了一件事,直接curl请求同样的端点,观察返回内容。下面这一步非常关键,它能帮我们把“SSH 问题”和“codex 应用问题”彻底分开。
2. 第一轮排查:从 SSH 到 codex 的网络链路
2.1 确认 SSH 链路本身没有问题
虽然已经 SSH 连上了,但仍要确认一下这台服务器的出网能力。因为 codex 在远程服务器上运行时,请求是从“服务器”发出去的,不是从你本地电脑发出去的。这一点很多人会忽略:本地能登录 codex,不代表服务器能登录 codex。
先确认 SSH 层的基本状态:
ssh -vvv your_user@your_server如果能看到debug1: Authentication succeeded,说明 SSH 没问题。另外检查一下服务器上的/etc/ssh/sshd_config里有没有奇怪的限制,比如AllowUsers把你限制在某个 IP 段,不过这通常不会导致 codex 403,只是顺手排除。
真正的重点在于:codex 需要访问自己的后端服务,而这些访问往往走 HTTPS 443 端口。如果服务器只开了 22 端口,出站 443 被防火墙或安全组拦截,codex 就会报连接类错误;但如果网关是统一返回 403 的,那就会伪装成权限错误。所以第一步要验证服务器能不能正常访问 HTTPS。
2.2 在远程服务器上直接测试 codex 的 API 端点
用 curl 直接打一下 codex 的认证或对话端点。注意,不同版本的 codex 端点略有差异,但总体都是 HTTPS 请求。你可以先抓日志里的完整 URL,然后用curl -i原样复制:
curl -i -X POST https://your-codex-endpoint/responses \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "ping"}'这里的-i会显示响应头,-v可以看更详细的握手信息。如果 curl 能正常返回 JSON,哪怕返回的是业务错误,也说明 443 出网没问题;如果 curl 直接返回 403,或者连接超时,那就要继续查网络。
那次我们遇到的情况很典型:本机 curl 直接返回 403,响应体里写着blocked by gateway。这就说明根本还没到 codex 真正的服务端,而是被中间的某个代理网关拦了。顺着这个线索,我们很快就发现服务器的环境变量里被人设置了一个HTTPS_PROXY,指向一个已经失效的内部代理地址。codex 客户端会读取这个环境变量,把请求都转发给那个代理,代理转发失败后直接回了 403。
给读者的建议:在远程服务器上执行下面的命令,看看有没有代理环境变量残留:
env | grep -i proxy比如http_proxy、https_proxy、HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、all_proxy。如果有,先不要急着删,可以临时把代理清掉再测试:
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy codex login如果清了代理之后能正常登录,那就说明 403 确实是代理导致的。这里要说明一下,我讲的都是正常的 HTTP(S) 代理场景,比如公司内网出口代理、调试用的本地代理工具,而不是任何特殊用途的工具。在企业开发环境里,代理配置错误是 403 的高发原因。
2.3 服务器时间不同步会让认证“偶然”失败
有次排查另一个服务时,发现一个特别隐蔽的问题:服务器时间比真实时间慢了大概 8 分钟。很多 token 签发和校验是与时间绑定的,客户端发起请求时带上过期时间戳,服务端校验时发现偏差超过宽容窗口,就会拒绝请求。有些服务返回 401,有些服务统一返回 403,甚至还会在日志里写一段类似 time skew 的提示。
所以,遇到 403 先顺手看一眼服务器时间:
date -u如果偏差明显,用 chrony 或 ntp 同步一下:
sudo systemctl status chronyd sudo chronyc makestep同步之后再跑 codex 登录,有时问题就莫名其妙消失了。时间偏差导致的认证失败,往往只在你执行登录的那一刻出现,平时 SSH 操作没影响,所以特别容易忽略。
3. codex 登录态与配置:为什么服务端会返回 403
3.1 登录态存在哪里,token 过期即 403
把网络层的问题排掉之后,就该看 codex 自己的登录态了。codex 的登录态一般保存在用户目录下,常见路径是~/.codex/auth.json或~/.codex/config.toml。里面存的是 API Key、Access Token、组织 ID 之类的信息。
如果你在远程服务器上安装 codex 之后,从没登录过,那 auth 文件可能压根不存在。这时候直接执行codex login,会走一个交互式流程。问题在于:很多远程服务器是没有桌面浏览器的,codex 的默认登录方式需要你在本地浏览器里打开一个授权链接,输完 code 之后再回到终端。如果这个流程被跳过,或者授权链接已经被使用过,服务端会拒绝请求,返回 403。
检查方式:
cat ~/.codex/auth.json如果文件存在,看一下expires_at或 token 创建时间。如果 token 已经过期,服务端在验证 JWT 签名时就会拒绝,返回 403。很多工具在 token 过期后并不会给一句“token expired”,而是直接给一个笼统的 403,这一点非常误导人。
顺便说一句,如果你在远程服务器上使用sudo执行了 codex 登录,然后又以普通用户身份登录,可能会出现“配置在 root 名下,但你用的是普通用户”的诡异现象。排查时记得确认当前用户是谁:
whoami echo $HOME登录态文件必须放在当前用户的$HOME/.codex下,否则客户端找不到正确的 token。
3.2 服务端风控与 IP、设备标识被拒
还有一种 403,纯粹是服务端风控在起作用。codex 这类 AI 编程工具的后端通常有比较严格的风控策略,比如数据中心 IP 段可能被列入高风险名单、某个 IP 在短时间内请求频率过高、或者组织管理员限制了某些成员的访问权限。
如果你是在一台云服务器上首次登录 codex,而之前从来没有在该 IP 段成功登录过,服务端可能把这个请求当成异常登录,直接返回 403。这时候服务端响应体里通常会给出提示,类似denied by risk control或device not trusted。你需要做的,不是反复重试,而是先确认当前服务器的出口 IP:
curl -4 -s https://ifconfig.me或者:
curl -4 -s https://ipinfo.io/ip拿到 IP 之后,再看看是否在 codex 服务端支持的区域或组织允许列表里。如果你所在网络环境本身有访问限制,请走企业合法的出口代理,而不是在服务器上挂任何非常规工具。我在这里不展开说,因为这不是技术重点,而且容易踩合规红线。
3.3 日志里的 local proxy 到底在说什么
回到文章标题里提到的那个日志:cc switch local proxy failed while handling codex endpoint /responses。这句话看着吓人,其实拆开就三块:
cc是 codex 客户端内部某个组件的代号,不用深究。local proxy指的是 codex 运行时配置的本地代理地址,通常是某种调试代理或者请求转发组件。failed while handling codex endpoint /responses表示它在处理/responses这个请求时,切换代理失败了。
换句话说,codex 客户端把请求发到了一个“本地代理”,但这个代理没有正常工作,导致连接失败或返回 403。排查思路很直接:找配置里有没有base_url、proxy_url、local_proxy之类的字段。常见位置:
cat ~/.codex/config.toml如果配置文件里有类似:
[api] base_url = "http://127.0.0.1:5090/v1"而这个 5090 端口本地根本没有服务监听,那所有请求都会失败。我们用ss -lntp看一下端口:
ss -lntp | grep 5090没有输出,就说明本地代理没有起来。要么注释掉这行配置,要么启动对应的代理服务。我们在排查时发现,同事之前为了调试某个特性,手动改过 config.toml,把请求指向了本地 5090 端口,后来忘了改回来。这就是“cc switch local proxy failed”的直接原因。
如果你在配置里看到类似host 5090 hostname 10.11.225.193 user user identityfile ...这样的内容,也别急着奇怪,那可能是 SSH config 里的逗号分隔写法被解析成了别的字段,但本质上不会导致 codex 403。遇到花里胡哨的配置,先备份,再清理,比反复对比要快得多。我当时的做法是:
cp ~/.codex/config.toml ~/.codex/config.toml.bak然后把疑似代理相关的一行注释掉,重新登录,问题解决。
4. 在远程无头服务器上正确使用 codex 登录
4.1 浏览器 OAuth 在 SSH 里行不通,改用设备码或 API Key
远程服务器没有图形界面,这是很多 codex 登录问题的根源。codex 默认的登录方式之一是在本地浏览器完成授权,但你在 SSH 终端里执行codex login时,它可能只会输出一段 URL,要求你复制到浏览器打开,然后输入一个一次性代码。如果在服务器终端里没法弹出浏览器,或者你根本没有本地浏览器,就很容易卡住。
更稳妥的方式是用支持“无头”登录的方式来认证。不同版本的 codex 参数略有差别,但通常有一个--headless标志:
codex login --headless或者直接使用 API Key:
export OPENAI_API_KEY="sk-xxxx" codex login注意,环境变量的方式只对当前会话有效,如果你重开一个 SSH 会话,环境变量会丢失。建议写进~/.bashrc或~/.zshrc,但要注意别把密钥提交到 Git 仓库里。我自己喜欢用的是配置文件形式,比如在~/.codex/auth.json里写清楚 key,然后把它设置成 600 权限:
chmod 600 ~/.codex/auth.json chmod 700 ~/.codex这个细节虽然简单,但真的能挡住 90% 因为权限过大导致的“配置无法读取”问题。很多工具为了安全性,会拒绝读取权限过大的密钥文件,如果读不到 token,自然就会 403。
4.2 配置 API Key 的正确姿势与环境变量清理
有些朋友在服务器上配置了 API Key,但还是 403,为什么?最常见的原因是环境变量名写错,或者配置里包含了多余的空格和引号。
比如你写:
export OPENAI_API_KEY="sk-xxxx"这个没问题。但如果你写成:
export OPENAI_API_KEY = "sk-xxxx"shell 会直接把整个字符串当成命令执行,自然也不行。更隐蔽的是配置base_url时写错了协议,比如把https://写成了http://,或者地址末尾多了一个/v1,导致请求路径变成了/v1/v1/responses,服务端找不到对应资源,可能返回 403。如果你不确定,就开 debug 日志看实际请求的 URL:
RUST_LOG=debug codex login或者你用的版本支持--verbose,也一并打开。我见过最离奇的一次,是配置文件里用了 Windows 风格的路径,比如C:\Users\yx\.ssh\id_rsa,在 Linux 服务器上直接解析失败,最终导致认证相关配置读取异常。虽然看起来是 SSH 密钥问题,实际上影响的是 codex 的配置加载流程。后来我把路径统一改成绝对路径,问题就没了。
4.3 配置文件权限、目录归属这类“看不见”的问题
用 root 还是普通用户登录,对 codex 的影响很大。假设你用root登录并执行了codex login,然后在另一个会话里用dev用户登录并尝试 codex,它会去读/home/dev/.codex/auth.json。如果这个文件不存在,就相当于没有 token,403 就来了。
反过来,如果你用dev用户登录,却用了sudo codex login,token 会写到/root/.codex/auth.json,dev用户的 codex 客户端同样读不到。
排查时可以用一条命令看当前用户下是否有配置:
ls -la ~/.codex/重点关注属主和权限:
stat -c "%U %G %a %n" ~/.codex/auth.json正常的配置应该是当前用户拥有,权限 600 或 400。如果出现 root 所有,但你以普通用户运行,那就重新以普通用户登录一次,或者把文件拷贝过来并改属主:
sudo chown -R dev:dev /home/dev/.codex这个细节几乎不会出现在官方文档里,但在实际运维中非常常见。当时我们排查了很久,最后发现是同事用sudo -i登录过,把一堆配置写到了 root 目录,后来切回普通用户,自然就 403 了。
4.4 防火墙、安全组与出站代理的核对
最后一个隐蔽点:云服务器的安全组。很多公司为了安全,会限制服务器对公网的出站访问,只允许 80/443 等少数端口。如果只允许 22 端口,而你在服务器上跑 codex,请求发不出去,会被网关拦截,返回 403。这种问题用 curl 测一下就知道。
另外,如果 codex 的客户端配置里指定了组织 ID,而你的 API Key 不属于该组织,服务端也会返回 403。检查一下当前登录的组织:
codex whoami如果显示的组织和你的 Key 不对应,那就要在登录时手动指定组织参数,或者在网页端把 Key 加到对应组织下。否则无论换什么网络,403 都会稳定复现。
5. 疑难杂症速查表与我的几点实战经验
5.1 问题现象、原因与处理方式速查表
整理一份速查表,方便以后遇到 403 时按图索骥。
| 现象 | 可能原因 | 快速排查与处理 |
|---|---|---|
| SSH 正常,codex login 返回 403 | token 过期 | 检查~/.codex/auth.json中的有效期,重新登录 |
| 日志里出现 local proxy failed | 配置了本地代理但端口未监听 | 检查 config.toml 的 base_url,注释掉或启动对应服务 |
| curl 直接 403,响应体有 blocked by gateway | 环境变量代理指向失效代理 | env | grep -i proxy,临时 unset 后重试 |
| 服务器时间偏差大 | JWT 时间戳校验失败 | date -u确认,用 chronyc 同步 |
| 用 sudo 登录后普通用户无法使用 | token 写错目录 | ls -la ~/.codex/,改属主或用普通用户重新登录 |
| 浏览器 OAuth 在无头环境卡住 | 没有浏览器完成授权 | 用--headless或配置 API Key |
请求 URL 是http:// | base_url 协议错误 | 改成https:// |
| 始终返回“organization not allowed” | Key 不属于当前组织 | 换 Key 或在组织下添加该 Key |
| IP 被风控拒绝 | 数据中心 IP 被列入风险名单 | 走企业合法出口代理,或更换对外 IP |
这张表不是万能的,但覆盖了我在 SSH + codex 场景里见过的大部分 403。核心思想就一句话:403 不是错误终点,而是入口,后面一定还有更具体的响应信息。
5.2 几个我亲手踩过的坑
第一个坑:把日志级别默认关了。codex 默认日志输出很克制,遇到 403 你不一定能看到背后的响应体。后来我养成习惯,在任何登录报错场景下,先把环境变量设成 debug:
export CODEX_LOG_LEVEL=debug codex login这样能看到请求头、响应头、代理配置等完整信息。很多官方文档不会教你这个,但比瞎猜快得多。
第二个坑:改配置时不备份。我那次把 config.toml 里的 base_url 注释掉之后,虽然解决了 403,但顺手把之前设置的模型参数也删了,导致 codex 后端模型找不到。后来所有配置文件改动,我都先备份成.bak,再慢慢改。
第三个坑:在服务器上测试时用了sudo,把问题弄复杂了。建议能不用 sudo 就不用 sudo,codex 配置放在普通用户目录里,不要纠缠于 root 和普通用户的权限关系。
第四个坑:过于相信“本机能用,服务器也能用”。服务器和本地电脑的网络环境、环境变量、DNS 解析、防火墙规则完全不同。凡是遇到 403,我都会先curl -v在服务器上打一次原始请求,看它到底到不到服务端。
5.3 最小化复现步骤,留着下次用
最后分享一下我的标准排查顺序,你直接照做就行:
# 1. 确认当前用户和目录 whoami echo $HOME # 2. 检查时间偏差 date -u # 3. 检查代理环境变量 env | grep -i proxy # 4. 检查 codex 配置和日志 ls -la ~/.codex/ tail -n 200 ~/.codex/logs/*.log # 5. 清理代理环境变量后重试 unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy codex login --headless如果这一步后还是 403,接着用 curl 打原始端点,拿到响应体,再回到速查表对应原因。这个流程我基本不跳步,因为少一步都有可能误判。
我在实际排查中体会最深的一点是:403 是一个极其“懒惰”的错误码,服务端不想暴露太多内部信息,中间代理也懒得做透传,最后呈现给你的就是一个干巴巴的 403。但只要你把它当作一次网络链路和认证状态的总检查,从 SSH 链路、代理环境变量、时间同步、登录态文件、服务端 IP 风控一路排查下来,大多数问题都能在十分钟内定位。
最后再分享一个小技巧:如果你修改了 codex 配置之后还是 403,可以试试完全退出进程再重新登录,因为 codex 的某些配置读取是一次性的,不会实时刷新。我在远程服务器上排查时,经常是改完配置忘了重开终端,然后浪费五分钟反复看同一个错误。先重启会话,再谈其他,往往会有惊喜。