Skip to content

Cloudflare Tunnel 的凭据缓存坑 ​

这是我遇到过最"反直觉"的一个坑:配置文件里明明写进去了,接口就是不认。

如果你也在用带 Web 管理界面的 Tunnel 应用(比如 NAS 应用中心里那种一键安装的 Cloudflare Tunnel),并且碰到过"新建 API 凭据之后一直报认证失败",这篇大概就是你的问题。

现象 ​

场景是这样:我要给第二个应用绑定域名,于是新建了一个 API 凭据(appId + appKey),然后用它调管理接口:

bash
curl -H "X-App-Id: $NEW_ID" -H "X-App-Key: $NEW_KEY" \
  http://127.0.0.1:19092/api/status

返回:

json
{"success": false, "message": "认证失败:无效的 X-App-Id 或 X-App-Key"}

第一反应是"我把 key 抄错了"。于是:

  1. 重新创建了一个凭据 —— 还是失败
  2. 换成旧凭据试 —— 旧凭据能用
  3. 直接 cat 凭据文件 —— 新凭据明明在里面,appId、appKey、appName 全对,创建时间也是刚才

新凭据在文件里存在、格式正确、旧凭据能用、新凭据不能用。到这里基本可以排除"操作错误"了。

根因 ​

去翻这个应用的二进制才发现,它的 HTTP API 服务(api_server)是这样工作的:

  1. 进程启动时,读取凭据文件,把整张表加载进内存(一个 credsMu 保护的 map)
  2. 之后所有认证请求都查内存表
  3. 没有文件监听(watcher),也没有 reload 接口

也就是说:管理界面通过 MCP 新建凭据时会立即写文件(所以 cat 能看到),但正在跑的 api_server 只在启动那一刻读过一次文件。它根本没有重新读的机会。

判断方法非常简单,对比两个时间:

bash
# 进程启动时间
ps -o pid,lstart,cmd -p $(pgrep -f api_server-linux-amd64)

# 凭据创建时间
cat /var/apps/<app-id>/etc/app_credentials.json | grep createdAt

如果进程启动时间早于凭据创建时间,那就是这个坑。

解法 ​

重启这个隧道应用。就这一条。

代价是重启期间所有绑定的域名会短暂 502/连不上(实测几秒到几十秒,取决于重连速度),所以别在高峰期做。

bash
# 在 NAS 应用中心停 → 启,或者用 CLI
# 重启后验证
curl -H "X-App-Id: $NEW_ID" -H "X-App-Key: $NEW_KEY" \
  http://127.0.0.1:19092/api/status

顺带记下的几个衍生结论 ​

排查这个问题的过程里,顺手验证了几件容易被文档误导的事,一并记下:

1. 认证只认 Header

同一份文档里写了"可以用 URL 参数传 key",但实测:

bash
# ❌ 报认证失败
curl "http://127.0.0.1:19092/api/status?appId=$ID&appKey=$KEY"

# ✅ 正常
curl -H "X-App-Id: $ID" -H "X-App-Key: $KEY" http://127.0.0.1:19092/api/status

只看文档会被 URL 参数那个写法坑掉半小时。

2. service 字段必须带 scheme

注册域名时如果只写 localhost:3000,Cloudflare 侧会报 1056(origin 配置无效)。要写 http://localhost:3000。

3. 一个 appName 只能绑一个域名

想给第二个服务绑域名,必须新建一个 appName(连同它的凭据),而不是在旧凭据上再 register 一次 —— 否则可能把原来那条规则覆盖掉,导致已经在用的域名突然 502。

4. 注册域名会顺带建 DNS 记录

不需要单独调 DNS 接口。反过来说:删除绑定不会自动清 DNS,残留的 CNAME 得去 Cloudflare 控制台手工删。

5. 验证 DNS 别被本地缓存骗

刚建的记录,用本机 nslookup 查经常是 NXDOMAIN —— 那是上游 DNS 的负缓存。用公共 DoH 验证:

bash
curl -s "https://doh.pub/dns-query?name=app.example.com&type=A" \
  -H "accept: application/dns-json"

或者干脆跳过 DNS,直接指定 IP 测:

bash
curl --resolve app.example.com:443:203.0.113.10 https://app.example.com/

教训 ​

这件事很典型地说明了一句话:"写进配置"不等于"生效"。

大多数时候我们排查故障的顺序是"配置对不对 → 网络通不通 → 权限够不够",但漏掉了最前面的一个问题:正在跑的那个进程,到底读没读过这份配置?

从那以后我遇到"配置明明没错但就是不生效"的情况,第一步固定是先看进程启动时间。这个习惯后来帮我省了不知道多少时间——包括 Docker 的 daemon 配置、nginx 的挂载文件、systemd 的环境变量,本质都一样。

一句话版本:新凭据不生效?重启那个进程,别怀疑自己抄错了。