cf-saas-tenant-router 部署教程:Cloudflare for SaaS 多租户路由,客户域名一条 CNAME 即接入

cf-saas-tenant-router 部署教程:Cloudflare for SaaS 多租户路由,客户域名一条 CNAME 即接入
luoli前言
做「给别人提供服务」的东西,迟早会撞上一个问题:客户想用自己的域名接入,但客户的域名不在你的 Cloudflare 账号里。
传统做法是让客户把域名 NS 迁过来——不现实。正确姿势是 Cloudflare 官方的 Cloudflare for SaaS:你提供入口,客户只加一条 CNAME 指向你,证书由 Custom Hostname 自动签发。
cf-saas-tenant-router 就是把这套能力做成了开箱即用的多租户系统:
- 项目地址:https://github.com/lzmss-love/cf-saas-tenant-router
- 技术栈:Cloudflare Workers + D1(三张表)+ Custom Hostnames + GitHub Actions
- 定位:客户域名不托管 CF,加一条 CNAME 即可把流量接入你的 Worker,按 Host 分流转发到各自的上游(tgproxy / cf-erp / cf-subcloud 等均可)
整条链路:
1 | 客户域名(NS 留在客户自己的 DNS) |
部署完成后你会得到:
| 资源 | 值 |
|---|---|
| 管理面板 | https://<宿主域名>(ADMIN_TOKEN 账号密码登录) |
| 客户自助接入页 | https://<宿主域名>/join |
| Worker | saas-tenant-router,*/* 路由挂宿主 zone |
| D1 | saas-tenants(租户 / 接入码 / 请求日志三张表) |
| DNS | service(fallback origin)、customers、opt*(优选入口,可多条) |
功能特性
管理面板(四个标签页)
| 标签 | 功能 |
|---|---|
| 租户 | 增删改查、详情(直接给出客户要加的 CNAME 指令)、启用/停用(suspended 即停止转发)、删除 |
| 接入码 | 生成 8 位码发给客户;客户在 /join 输码 + 域名自助完成接入;一次性、防重放、可作废 |
| 自定义主机名 | 创建/删除 Custom Hostname;证书与主机名状态一目了然;详情弹窗直接给出客户要补加的全部 DNS 指令(验证 TXT / DCV 委派 CNAME) |
| 优选 | 多路管理(opt / opt2 / opt3…,A 记录或 CNAME 双形态)+ 在线测速(当前线路真实延迟)+ 快捷切换社区优选域名 |
几个设计细节(源码确认)
- 接入码防呆:8 位随机码,字符表去掉了
0 O 1 I L这类易混淆字符,口头报给客户也不会念错 - 租户状态机:
pending(没配上游,仅标记)/active(配了 upstream,正常转发)/suspended(管理员停用,立即停止转发)/ 软删除 - 上游两级配置:租户表里每行可配
upstream_base(租户专属),没配则回落到全局变量TENANT_UPSTREAM_BASE;两者都没有时返回提示页,而不是报 1016 - 面板与 API 只认宿主域名:管理接口只在宿主 zone(及可选的 workers.dev 后缀)上响应,且需要
ADMIN_TOKEN(格式账号:密码),客户域名上根本访问不到 - 兼容严格上游:对 tgproxy 这类做 bridge 校验的服务,Worker 会按上游真实 hostname 重算 cap——客户手里任何域名版本的接入凭证都能过认证
部署前准备(人工三件套,无法 API 化)
无论用哪种部署方式,每个 Cloudflare 账号都要先手工完成这三步:
- 宿主域名加入该 CF 账号(NS 托管,Free 计划即可)
- Enable Cloudflare for SaaS:控制台 → SSL/TLS → Custom Hostnames → Enable(需要绑卡——这是 CF 唯一不给 API 的步骤)
- Fallback Origin:同一页面 → Fallback Origin 栏填
service.<你的域名>→ 等 Active。注意这一步只能控制台点,API 会报 10405(CF 硬限制)
然后建一个 API Token(控制台 → My Profile → API Tokens)。CLOUDFLARE_API_TOKEN 一共需要 11 项权限,建 Token 时对着下面的清单逐项勾选即可(级别列标注了它挂在「账户」还是「区域」下):
| # | 级别 | 权限名 | 用途 |
|---|---|---|---|
| 1 | 账户 | D1 Write | 自动建 / 迁移 saas-tenants 数据库 |
| 2 | 账户 | D1 Read | 查询现有 D1 实例,避免重复建库 |
| 3 | 账户 | Workers Scripts Write | wrangler deploy 部署 / 更新 Worker 代码 |
| 4 | 账户 | Workers Scripts Read | 读取现有 Worker 与 secret 状态 |
| 5 | 区域 | DNS Write | 自动建 / 改 service、customers、opt* 记录 |
| 6 | 区域 | DNS Read | 读取现有 A / CNAME 记录做幂等比对 |
| 7 | 区域 | SSL and Certificates Write | 面板创建 / 删除 Custom Hostname(缺了会 403) |
| 8 | 区域 | SSL and Certificates Read | 面板展示证书签发状态 |
| 9 | 区域 | Workers Routes Write | 写入 */* 路由 |
| 10 | 区域 | Workers Routes Read | 校验路由是否已挂到本 Worker |
| 11 | 区域 | Zone Read | 自动查 zone id |
Zone Resources 只选你的宿主 zone(账户级的 D1 / Workers Scripts 权限跟随账户,区域级权限锁定该 zone),最小权限。
部署路线 A:本机一条命令初始化(推荐)
仓库是模板化的:拿到任何新 CF 账号,域名/账号/D1 全部自动配置,代码零修改。
1 | # 1) clone 到一个新目录(重要:不要在已有部署的目录里跑) |
init.mjs 会自动完成:查 zone id → 建 D1 → 重写本目录的 wrangler.jsonc(routes / D1 / vars 全部指向新账号)→ 建 DNS 记录(fallback origin / customers / opt)→ 打印剩余的手动步骤。
然后收尾两件事:
1 | npx wrangler deploy # 部署 Worker + 路由 |
⚠️
init.mjs会重写当前目录的 wrangler.jsonc。在 clone 出来的新目录里跑,不要在已有部署的目录执行,否则当前账号的配置会被覆盖。
部署路线 B:手动部署(想搞懂每一步)
1 | npm i |
三个 secret(存 CF 侧)
1 | npx wrangler secret put ADMIN_TOKEN # 面板登录,格式 账号:密码 |
关键纪律:一切凭证走 secret,不要在 Dashboard 建 plain text 变量——wrangler deploy 会用 wrangler.jsonc 的 vars 段整体替换明文变量,Dashboard 里建的会被清掉。secret 不受影响。
wrangler.jsonc 的 vars 段(配置全在这)
| 变量 | 说明 |
|---|---|
OWN_ZONE / OWN_ZONE_ID |
宿主域名与 zone id |
CNAME_TARGET |
客户 CNAME 指向的入口,默认 opt.<宿主域名> |
OWN_WORKERSDEV_SUFFIX |
workers.dev 验证子域后缀(可选,测试期用) |
TENANT_UPSTREAM_BASE |
全局默认上游(可选,租户级 upstream_base 优先) |
OPT_PRESETS |
社区优选域名预设(如 vivid43.cf.090227.xyz) |
OPT_TEST_HOST |
在线测速用的已激活租户域名(可选) |
配置全部在 vars 里,代码零硬编码。改完 npx wrangler deploy 生效。
验证
1 | curl https://<宿主域名>/health |
浏览器打开 https://<宿主域名>,能用 ADMIN_TOKEN 登录面板即部署成功。
部署路线 C:GitHub Actions 全自动(push 即上线)
仓库自带 .github/workflows/deploy.yml,push 到 master(或手动触发)自动完成全流程:
1 | 查 zone id → 建/找 D1 → 渲染配置 → 部署 Worker → D1 迁移 |
一次性配置
- 建部署专用 API Token:权限与上文「部署前准备」的 11 项清单完全一致(D1 / Workers Scripts / DNS / SSL and Certificates / Workers Routes 的读写 + Zone Read),Zone Resources 选宿主 zone
- GitHub 仓库 → Settings → Secrets and variables → Actions,配这些 repository secret:
| Secret | 值 | 必填 |
|---|---|---|
CF_ZONE |
宿主域名(如 xxx.com) |
✅ |
CLOUDFLARE_API_TOKEN |
上一步的 token | ✅ |
CLOUDFLARE_ACCOUNT_ID |
Account ID | ✅ |
CF_ADMIN_TOKEN |
面板密码(账号:密码) |
✅ |
CF_DNS_TOKEN |
专用 DNS token(不填则复用部署 token) | ⬜ |
CF_TGPROXY_SECRET |
TG WEB 代理密码(不接 TG 不用填) | ⬜ |
CF_WORKERS_DEV_SUFFIX |
workers.dev 子域后缀(测试期才用) | ⬜ |
CF_OPT_TEST_HOST |
在线测速用的已激活租户域名 | ⬜ |
zone id / D1 id / CNAME target 都不用填——Actions 自动解析,D1 不存在会自动创建。
单仓库管多个 CF 账号(Environments)
这是这套 CI 的亮点:每个账号一个 GitHub Environment,手动触发时 target 自由输入账号名:
- Settings → Environments → New environment,名称填账号名(如
maowei) - 在该 environment 的 Secrets 里配置该账号的一套 secrets
- Actions → Deploy → Run workflow → target 填
maowei→ 部署到对应 CF 账号
指定 target 时,zone/D1 会按 zone 名在目标账号实时解析(忽略仓库级回退 ID,避免跨账号错绑路由)。新增账号不需要改任何代码;push 自动部署不受影响(无 target = 仓库级 secrets = 主账号)。
使用说明
新客户接入流程(约 3 分钟)
面板「接入码」→ 生成 → 发给客户(复制按钮自带接入地址)
客户打开
https://<宿主域名>/join→ 输接入码 + 自己的域名 → 页面直接给出要加的 CNAME:1
<客户域名> CNAME opt.<宿主域名>
客户在他的 DNS 服务商添加这条记录。注意两点:不能用 A 记录;apex 根域名不支持——让客户用子域(如
app.客户域名)面板「自定义主机名」→ 为该 host 创建 Custom Hostname → 点详情,把弹窗里的 DNS 指令发给客户补加(验证 TXT 或 DCV 委派 CNAME,按客户情况二选一)
等证书与主机名两个状态都变绿。如果证书一直「待验证」:查客户域名的 CAA 记录(会沿 DNS 树向上继承),需要授权签发 CA,例如加
0 issue "ssl.com",或删除现有 CAA
完事之后 Worker 已经自动识别新租户,流量按其 upstream_base 转发。
日常管理
停用某个客户:租户页 → 停用(状态变
suspended,立即停止转发,且不影响其 Custom Hostname 证书)给某客户换上游:编辑该租户的
upstream_base,立即生效,无需重新部署查某个客户为什么打不开:D1 里有
req_log请求日志(method / path / query / UA / IP / status),直接看客户端的真实请求形态:1
2npx wrangler d1 execute saas-tenants --remote \
--command "SELECT * FROM req_log ORDER BY id DESC LIMIT 20"面板按钮没反应:面板是单文件 HTML 内嵌 JS,先跑
node scripts/check-embedded-js.mjs做语法检查再部署
优选 IP 维护
优选解决的是「国内访问 CF 边缘入口慢」:客户域名解析到测速优选的 CF IP,SNI 路由不变。
1 | CF_API_TOKEN=xxx npm run optimize |
- 对 24 个候选 IP 测 TLS 握手 → 自动把最快的写入优选记录(IP 没变则跳过写入)
- 换 IP 只改 DNS 记录,客户域名不用动,代码不用动
- 优选入口支持两种形态:A 记录(直指优选 IP)或 CNAME(指社区优选域名如
*.cf.090227.xyz,自动更新更省心);面板更新时保持原类型 - 建议每周跑一次;如果某天客户集体打不开(IP 失效),立即跑一次
- 非标端口的反代节点(如
IP:26524)对 TG WEB 类客户端无用(固定 443 且无端口字段),仅适合支持自定义端口的客户端
面板「优选」标签页也可以直接做多路管理(opt / opt2 / opt3… 增删改、切换 A/CNAME、在线测速),不用碰命令行。
常见问题速查
| 现象 | 原因 / 处理 |
|---|---|
| 客户域名返回占位页 “Connection” | bridge cap 与 hostname 不匹配——Worker 会自动重算;若仍出现,查 req_log 看请求形态 |
| 面板登录失败 | ADMIN_TOKEN 是 secret(账号:密码);确认没在 Dashboard 建同名明文变量(会被部署清掉) |
| Custom Hostname 操作 403 | CF_API_TOKEN 缺 Zone → SSL and Certificates → Edit 权限,重建 token 更新 secret |
| 证书一直「待验证」 | 客户域名或其上级的 CAA 记录未授权签发 CA——加 0 issue "ssl.com" 或删除现有 CAA |
| Fallback Origin API 报 10405 | 该端点不接受 API Token(CF 硬限制)——必须控制台手点 |
wrangler deploy 后变量消失 |
部署整体替换明文 vars——凭证一律走 secret |
| 客户用 apex 根域名接入失败 | Custom Hostname 不支持 apex,让客户改用子域 |
安全注意事项
ADMIN_TOKEN用随机长串,它是面板与管理 API 的唯一门禁;管理接口本身也只在宿主域名上暴露CF_API_TOKEN最小权限:Zone Resources 锁定宿主 zone,别用 Global API Key- 凭证一律走 secret,不要写进
wrangler.jsonc明文 vars(会被 push 和部署清掉,还可能随模板仓库泄漏) - 客户接入码一次性 + 可作废,泄露了直接在面板作废重新生成
req_log是诊断表,会记录客户请求的 path 与 UA——自己留档没问题,对外分享日志时注意脱敏
小结
这套项目把「Cloudflare for SaaS 多租户」这件事从一堆控制台操作变成了三条可重复的路:本机 init 一键初始化、手动部署搞懂原理、Actions push 即上线,再配上接入码自助接入、面板化租户管理、自动优选 IP。适合的场景很明确:你要给多个客户/多个域名提供同一种入口服务(代理入口、自建应用、API 网关都行),又不想让客户迁移域名托管。
部署口径总结成一行:宿主域名托管 + Enable for SaaS + Fallback Origin 三件套 → init 或 Actions → 发接入码 → 客户加一条 CNAME → 建 Custom Hostname → 双绿灯收工。
参考资料
- cf-saas-tenant-router 项目仓库(GitHub)
- Cloudflare for SaaS 文档(Custom Hostnames)
- Cloudflare Workers D1 文档
- Wrangler CLI 文档
- Cloudflare API Token 权限说明
- GitHub Actions:Environments 使用










