最近在自建 LiteLLM Proxy 时遇到了一个比较有意思的问题:
同一个模型,通过服务器本机直接访问 LiteLLM 一切正常,但经过域名和 Cloudflare 后,请求却稳定返回:
502 Bad Gateway
开始时我怀疑过 Cloudflare、LiteLLM、Responses API,甚至一度怀疑是不是最近公网扫描流量导致服务异常。
最后经过逐层排查发现:
真正返回 502 的并不是 Cloudflare,也不是 LiteLLM,而是 Nginx。LiteLLM 返回的 Response Header 太大,超过了 Nginx 默认的 upstream header buffer。
最终的错误日志非常明确:
upstream sent too big header while reading response header from upstream
这篇文章记录完整的定位过程。
我的部署结构
大致架构如下:
Client
│
▼
Cloudflare
│
▼
Nginx :443
│
▼
LiteLLM :6111
│
▼
OpenAI / Codex Backend
LiteLLM 运行在 Docker 中:
litellm 0.0.0.0:6111->4000/tcp
也就是:
Host :6111
↓
Container :4000
Nginx 则负责:
https://one.fullstar.tech
↓
http://127.0.0.1:6111
问题表现
通过公网域名请求 LiteLLM:
https://one.fullstar.tech/v1/responses
会返回:
HTTP/2 502
server: cloudflare
Cloudflare 的错误页面显示:
Browser Working
Cloudflare Working
Host Error
这很容易让人第一反应认为是:
- LiteLLM 挂了
- Cloudflare 回源失败
- Docker 网络异常
- LiteLLM Responses API 有 Bug
- 上游 Codex API 不稳定
但需要继续逐层排除。
第一步:确认实际 LiteLLM 端口
首先查看 Docker 端口:
sudo docker ps --format "table {{.Names}}\t{{.Ports}}"
得到:
NAMES PORTS
LibreChat 0.0.0.0:3080->3080/tcp
litellm 0.0.0.0:6111->4000/tcp
因此:
3080 → LibreChat
6111 → LiteLLM
第二步:绕过 Nginx 和 Cloudflare,直接测试 LiteLLM
接下来直接请求本机 LiteLLM。
我使用的测试 Payload:
{
"model": "gpt-5.6-terra",
"input": [
{
"role": "user",
"content": "Reply with exactly OK."
}
],
"reasoning": {
"effort": "medium"
},
"stream": true
}
完整测试命令:
curl -v http://127.0.0.1:6111/v1/responses \
-H "Authorization: Bearer YOUR_LITELLM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"input": [
{
"role": "user",
"content": "Reply with exactly OK."
}
],
"reasoning": {
"effort": "medium"
},
"stream": true
}'
结果:
HTTP/1.1 200 OK
server: uvicorn
content-type: text/event-stream
并且 SSE 正常返回:
data: {"type":"response.output_text.delta", ... "delta":"OK" ...}
最终:
OK
这一步非常重要。
它证明:
LiteLLM ✅
Docker 端口映射 ✅
gpt-5.6-terra ✅
Responses API ✅
Streaming ✅
LiteLLM → 上游 Codex ✅
所以问题已经可以缩小到:
Cloudflare / Nginx
这一层。
第三步:测试 Chat Completions
为了排除是不是 /v1/responses 特有问题,我又测试:
/v1/chat/completions
例如:
curl -v http://127.0.0.1:6111/v1/chat/completions \
-H "Authorization: Bearer YOUR_LITELLM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"messages": [
{
"role": "user",
"content": "Reply with exactly OK."
}
],
"stream": true
}'
结果同样正常。
所以 LiteLLM 本身基本可以完全排除。
第四步:通过 Cloudflare 域名请求
接下来发送完全相同的 Responses 请求,只把地址改成:
https://one.fullstar.tech/v1/responses
此时得到:
HTTP/2 502
server: cloudflare
cf-ray: ...
问题出现。
此时链路为:
Client
↓
Cloudflare
↓
Nginx
↓
LiteLLM
但还无法确定究竟是:
Cloudflare → Nginx
还是:
Nginx → LiteLLM
出现问题。
第五步:使用 –resolve 绕过 Cloudflare
这是整个排障过程中非常关键的一步。
使用 curl 的:
--resolve
直接把:
one.fullstar.tech
解析到服务器真实公网 IP,而不是 Cloudflare IP。
例如:
curl -vk \
--resolve one.fullstar.tech:443:YOUR_SERVER_IP \
https://one.fullstar.tech/v1/responses \
-H "Authorization: Bearer YOUR_LITELLM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"input": [
{
"role": "user",
"content": "Reply with exactly OK."
}
],
"reasoning": {
"effort": "medium"
},
"stream": true
}'
这时候请求路径变成:
Client
│
│ 绕过 Cloudflare
▼
Server Public IP :443
│
▼
Nginx
│
▼
LiteLLM
结果依然是:
HTTP/2 502
server: nginx/1.18.0 (Ubuntu)
这一步直接证明:
Cloudflare 是无辜的。
因为完全绕过 Cloudflare 后,Nginx 自己仍然返回 502。
因此问题正式缩小到:
Nginx
↓
LiteLLM
第六步:检查 Nginx proxy_pass
查看 Nginx 配置:
sudo nginx -T | grep -A 40 -B 10 "one.fullstar.tech"
配置为:
server {
listen 443 ssl;
server_name one.fullstar.tech;
client_max_body_size 128m;
client_body_timeout 300;
ssl_certificate /etc/nginx/cert/one/one.crt;
ssl_certificate_key /etc/nginx/cert/one/one.key;
location / {
add_header Access-Control-Allow-Origin *;
proxy_pass http://127.0.0.1:6111;
}
}
proxy_pass 没有问题:
Nginx
↓
127.0.0.1:6111
↓
LiteLLM
而 127.0.0.1:6111 本身也已经通过 curl 验证可以正常访问。
于是只剩 Nginx 处理 upstream response 本身的问题。
第七步:查看 Nginx error.log
运行:
sudo tail -n 100 /var/log/nginx/error.log
终于看到真正的错误:
upstream sent too big header while reading response header from upstream
而且不是偶发一次,而是大量重复:
upstream sent too big header while reading response header from upstream,
server: one.fullstar.tech,
request: "POST /v1/responses HTTP/2.0",
upstream: "http://127.0.0.1:6111/v1/responses"
问题完全定位。
根因:LiteLLM Response Header 太大
为什么 LiteLLM 的 Header 会这么大?
直接访问 LiteLLM 时可以看到大量 Header,例如:
x-litellm-call-id
x-litellm-model-id
x-litellm-model-name
x-litellm-model-api-base
x-litellm-version
x-litellm-response-duration-ms
...
除此之外,上游 Codex Backend 的 Response Header 也被 LiteLLM 带了出来:
llm_provider-x-codex-active-limit
llm_provider-x-codex-plan-type
llm_provider-x-codex-primary-used-percent
llm_provider-x-codex-primary-reset-after-seconds
llm_provider-x-codex-primary-reset-at
...
其中甚至包含非常长的:
llm_provider-x-codex-turn-state
以及 Cookie 等 Header。
最终形成:
LiteLLM
│
│ 巨大的 Response Header
▼
Nginx
│
│ 默认 proxy header buffer 不够
▼
upstream sent too big header
│
▼
502 Bad Gateway
这也很好地解释了为什么:
curl 127.0.0.1:6111
完全正常。
因为此时根本没有经过 Nginx。
最终解决方案
修改:
/etc/nginx/sites-enabled/one.conf
原始配置:
location / {
add_header Access-Control-Allow-Origin *;
proxy_pass http://127.0.0.1:6111;
}
改成:
location / {
add_header Access-Control-Allow-Origin *;
proxy_pass http://127.0.0.1:6111;
proxy_http_version 1.1;
proxy_buffer_size 64k;
proxy_buffers 8 64k;
proxy_busy_buffers_size 128k;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
其中解决这次 502 最关键的是:
proxy_buffer_size 64k;
因为它决定 Nginx 读取 upstream response header 时使用的缓冲区大小。
同时考虑到 LiteLLM 使用 SSE Streaming:
Content-Type: text/event-stream
关闭:
proxy_buffering off;
也更加合适。
并把长连接 timeout 调大:
proxy_read_timeout 600s;
proxy_send_timeout 600s;
检查配置
修改完成后先检查语法:
sudo nginx -t
得到:
nginx: configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
然后 reload:
sudo systemctl reload nginx
不需要重启 LiteLLM。
再次测试
再次发送:
curl -v https://one.fullstar.tech/v1/responses \
-H "Authorization: Bearer YOUR_LITELLM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-terra",
"input": [
{
"role": "user",
"content": "Reply with exactly OK."
}
],
"reasoning": {
"effort": "medium"
},
"stream": true
}'
此时请求恢复正常。
整个链路变成:
Client
↓
Cloudflare
↓
Nginx
↓
LiteLLM
↓
Codex Backend
↓
SSE Streaming
↓
OK
问题解决。
整个定位过程总结
这次最有效的不是不断修改配置,而是逐层缩小问题范围。
完整过程可以总结为:
1. 公网域名请求
↓
502
2. 直接请求 127.0.0.1:6111
↓
200
↓
排除 LiteLLM
3. 测试 Responses + Chat Completions
↓
都正常
↓
排除 endpoint compatibility 问题
4. 通过 Cloudflare 请求
↓
502
5. curl --resolve 绕过 Cloudflare
↓
Nginx 仍然 502
↓
排除 Cloudflare
6. 检查 proxy_pass
↓
127.0.0.1:6111 配置正确
7. 查看 /var/log/nginx/error.log
↓
upstream sent too big header
8. 增大 proxy_buffer_size
↓
reload nginx
9. 再次测试
↓
200 OK
也就是:
最开始怀疑:
Cloudflare?
LiteLLM?
Responses API?
Codex?
Docker?
最终:
Nginx proxy response header buffer 太小。
几个值得记录的经验
1. Cloudflare 的 502 页面不等于 Cloudflare 出问题
如果看到:
Browser Working
Cloudflare Working
Host Error
应该优先考虑:
Origin / Reverse Proxy
Cloudflare 很可能只是把源站返回的错误展示出来。
2. curl --resolve 非常适合排查 CDN / 反代问题
例如:
curl --resolve example.com:443:ORIGIN_IP https://example.com/
可以在保持:
Host
SNI
HTTPS
基本不变的情况下直接访问真实源站。
对于判断:
到底是 Cloudflare 有问题
还是我的服务器有问题
非常有效。
3. 一定要直接测试 upstream
如果:
Nginx → LiteLLM
出问题,那么第一件事应该测试:
curl http://127.0.0.1:6111
不要从最外面的 Cloudflare 开始猜。
4. error.log 往往比 502 页面有价值
浏览器只能告诉我:
502 Bad Gateway
但:
/var/log/nginx/error.log
直接告诉我:
upstream sent too big header while reading response header from upstream
两者的信息量完全不是一个级别。
5. AI API 的 Response Header 可能比普通 Web API 大很多
尤其是存在:
LiteLLM
+
Provider metadata
+
Codex usage metadata
+
Turn state
+
Streaming
这类组合时,不能默认认为 Nginx 的默认 buffer 一定够用。
最终配置
目前 LiteLLM 的 Nginx location 配置如下:
location / {
add_header Access-Control-Allow-Origin *;
proxy_pass http://127.0.0.1:6111;
proxy_http_version 1.1;
proxy_buffer_size 64k;
proxy_buffers 8 64k;
proxy_busy_buffers_size 128k;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
如果之后再遇到类似:
502 Bad Gateway
我的排查顺序会直接变成:
本机 upstream
→ 绕过 CDN
→ Nginx error.log
→ 最后才研究应用层
比一开始从 Cloudflare、LiteLLM 和模型 API 一层层猜要快得多。
By the way:LiteLLM 公网接口还在被自动化扫描
排查 502 的过程中,还顺手发现了另一个问题。
LiteLLM 的 Request Logs 中突然出现了大量失败请求,而且请求频率明显异常。
更关键的是,请求中使用的 Key Hash / Key 值呈现出非常明显的字典枚举特征,例如:
12345
123456
123456789
111111
000000
sk12345
sk123456
sk666666
...
同时这些请求具有几个共同特点:
Status Failure
Duration 0.00
Cost -
并且短时间内出现了大量不同的 Request ID / Session ID。
这种模式基本可以判断为:
公网扫描器发现了 OpenAI Compatible API Endpoint,随后开始使用常见弱 Key 对接口进行自动化枚举。
严格来说,这更准确地叫:
API scanning
+
credential guessing
+
dictionary attack
而不是已经“入侵成功”。
从当时的 LiteLLM 日志来看,这些请求全部失败,也没有产生模型调用 Cost,因此没有证据表明攻击者真正获得了有效 API Key。
但既然 LiteLLM 已经直接暴露到了公网,还是顺手加了一层 Cloudflare 防护。
第一层:LiteLLM 自己的 API Key
原本的第一道防线仍然是 LiteLLM:
Internet
↓
LiteLLM
↓
API Key 校验
只要 API Key 是足够长的随机值,而不是:
123456
password
litellm
sk123456
这类弱密码,那么普通字典爆破成功的概率实际上非常低。
但问题在于:
即使猜不中 Key,攻击请求还是会真实到达自己的服务器。
也就是说,它仍然会:
- 建立连接
- 进入 Nginx
- 到达 LiteLLM
- 触发认证逻辑
- 写 Request Log
- 消耗一定 CPU / IO / 数据库资源
因此更合理的方式是:
尽量在 Cloudflare 边缘节点就把明显异常的请求拦掉。
最终结构变成:
Internet
↓
Cloudflare
↓
WAF / Rate Limit
↓
Nginx
↓
LiteLLM API Key
↓
LLM Provider
第二层:给 /v1/* 增加 Rate Limit
首先在 Cloudflare 中进入:
Security
→ WAF
→ Rate limiting rules
针对 LiteLLM 的 OpenAI Compatible Endpoint:
/v1/*
增加限流。
我的思路是按来源 IP 独立计数,例如:
30 requests / 10 seconds / IP
达到阈值后:
Block 10 seconds
也就是:
某个 IP
↓
10 秒内请求超过 30 次
↓
Cloudflare Block
这里没有把阈值设置得特别低。
原因是 LiteLLM 后面可能接:
LibreChat
OpenCode
Codex
Agent
并行 Tool Calls
正常使用时,本身就有可能在短时间产生多次 API 请求。
如果设置成:
5 requests / 10s
很容易误伤自己的 Agent。
所以 Rate Limit 的目标不是:
每一个扫描请求都挡住。
而是:
防止单 IP 高频撞库以及异常请求洪泛。
第三层:按国家限制私人 LiteLLM
由于这个 LiteLLM 实际上是私人服务,因此还可以采用一个非常简单但有效的策略:
只允许自己可能使用的国家访问。
例如我平时只需要:
China
United States
于是增加了一条 Cloudflare Custom Rule。
逻辑为:
Hostname = one.fullstar.tech
AND
Country != China
AND
Country != United States
Action:
Block
等价 expression 可以写成:
(http.host eq "one.fullstar.tech"
and not ip.src.country in {"CN" "US"})
效果就是:
CN → Allow
US → Allow
其他国家 → Block
而且因为加入了:
http.host eq "one.fullstar.tech"
所以不会影响 fullstar.tech 下的其他网站。
这对于私人 API 是一种性价比非常高的防御方式。
第四层:管理后台和 API 应该区别对待
LiteLLM 本身其实存在两种完全不同的流量:
/v1/*
这是机器调用的 API。
而:
/ui
则属于人使用的管理后台。
因此它们不应该使用完全一样的 Cloudflare 策略。
例如:
/v1/*
不适合直接加:
Managed Challenge
因为 OpenCode、LibreChat、Codex 等程序并不会像浏览器一样完成 Cloudflare 人机验证。
否则:
正常 API Client
↓
Cloudflare Challenge
↓
无法通过
↓
API 全挂
所以 /v1/* 更适合:
Rate Limit
+
Geo Restriction
+
LiteLLM API Key
而 /ui 这种管理后台则可以考虑:
Cloudflare Access
或者:
Managed Challenge
甚至只允许自己的固定 IP。
一个额外的安全问题:不要让源站绕过 Cloudflare
这里还有一个很重要的问题。
我的 LiteLLM Docker 当时是:
0.0.0.0:6111->4000/tcp
这意味着 LiteLLM 的 6111 端口绑定在所有网卡上。
如果云厂商 Security Group / iptables 同时允许公网访问 6111,那么攻击者理论上可以直接请求:
http://SERVER_IP:6111
此时整个 Cloudflare:
WAF
Rate Limit
Country Block
都会被直接绕过。
因为请求路径变成了:
Attacker
↓
SERVER_IP:6111
↓
LiteLLM
而不是:
Attacker
↓
Cloudflare
↓
Nginx
↓
LiteLLM
因此更合理的方式是只让 LiteLLM 监听本机:
ports:
- "127.0.0.1:6111:4000"
这样:
公网 → 6111
无法直接访问。
只有:
Nginx → 127.0.0.1:6111
可以连接 LiteLLM。
或者至少在云厂商安全组中关闭公网 6111 端口。
最终真正希望得到的结构是:
Internet
│
▼
Cloudflare
│
┌────────────┴────────────┐
│ │
Rate Limit Custom Rules
│ │
└────────────┬────────────┘
▼
Nginx
│
▼
127.0.0.1:6111
│
▼
LiteLLM
│
API Key Auth
│
▼
LLM Provider
这样 Cloudflare 才真正成为第一层入口,而不是一个可以轻易绕过去的 CDN。
有趣的是:攻击和 502 最终没有关系
一开始看到:
大量异常 API 请求
+
Cloudflare 502
很容易自然地产生一个联想:
LiteLLM 是不是被攻击打挂了?
但经过完整排查以后发现,这其实是两个同时发生、但彼此独立的问题。
安全问题
公网扫描
→ 大量弱 Key 枚举
→ LiteLLM 全部鉴权失败
解决:
Cloudflare Rate Limit
+
Geo Restriction
+
强 API Key
+
关闭源站公网端口
502 问题
LiteLLM Response Header 很大
→ Nginx proxy buffer 不够
→ upstream sent too big header
→ Nginx 502
→ Cloudflare 展示源站 502
解决:
proxy_buffer_size 64k;
proxy_buffers 8 64k;
proxy_busy_buffers_size 128k;
这也是这次排障里比较有价值的一点:
两个问题恰好同时出现,并不意味着它们存在因果关系。
如果因为看到攻击流量就直接认定:
502 = 被攻击导致
反而会把排查方向带偏。
最终 LiteLLM 的防御思路
目前这套私人 LiteLLM 可以简单总结成四层:
第一层
Cloudflare Geo Restriction
→ 非预期国家直接 Block
第二层
Cloudflare Rate Limit
→ 高频请求直接 Block
第三层
Nginx
→ 唯一公网 Reverse Proxy
第四层
LiteLLM API Key
→ 最终认证
同时:
LiteLLM :6111
本身不应该直接暴露公网。
对于个人自建 LiteLLM 来说,这套方案已经可以挡掉相当大一部分互联网上无差别的:
端口扫描
API 扫描
弱密码枚举
低成本撞库
脚本滥用
而正常的 LibreChat / OpenCode / Codex API 请求基本不受影响。