Sub-Store Azure + Cloudflare Access 部署实践
在 Azure VM 上部署 Sub-Store 后端,通过 Caddy 反向代理 + Cloudflare DNS 代理 + Cloudflare Access 实现安全访问控制,解决 Basic Auth 认证循环弹窗问题。
[!info] related notes
- 前置笔记: [[sub-store|Sub-Store]], Cloudflare, Certbot
- 相关 MOC: 代理节点架构
- 相关资源: 哪吒面板部署, 单机部署实践
Sub-Store Azure + Cloudflare Access 部署实践
目标
在单台 Azure VM(Debian 12)上部署 Sub-Store 订阅管理服务,实现管理后台安全访问、订阅链接公开拉取,同时保证暴露面最小化。
架构设计
用户浏览器
│
▼
Cloudflare Edge(DNS 代理 + Access 认证)
│
▼ HTTPS 回源(仅 Cloudflare IP 可达)
Azure VM (Debian 12)
├── Caddy(反代 + TLS)
│ ├── admin.bakersean.top
│ │ ├── / → reverse_proxy → sub-store.vercel.app(前端)
│ │ └── /<PREFIX>/* → reverse_proxy → 127.0.0.1:3000(后端 API)
│ ├── substore.bakersean.top
│ │ └── /share/* → reverse_proxy → 127.0.0.1:3000(公开分享)
│ └── nezha.bakersean.top → reverse_proxy → 127.0.0.1:8008
└── Sub-Store 后端(Node.js,仅监听 127.0.0.1:3000)
核心原则:所有服务只监听 localhost,对外仅暴露 Caddy,通过 UFW 限制只有 Cloudflare IP 可以回源。
前置条件
- Azure VM(Debian 12),仅开放 SSH(22/tcp)
- 域名已托管到 Cloudflare,DNS 代理已开启(橙色云朵)
- Azure NSG 已放行 22/80/443(TCP)
部署步骤
1. 安装依赖
# Node.js 22(NodeSource 官方脚本)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Caddy(Debian 官方源)
sudo apt-get install -y caddy
# 防火墙 + 防爆破
sudo apt-get install -y ufw fail2ban
2. 构建 Sub-Store 后端
# 创建独立运行用户
sudo useradd --system --create-home --home-dir /var/lib/sub-store \
--shell /usr/sbin/nologin substore
sudo install -d -o substore -g substore /opt/sub-store /var/lib/sub-store /etc/sub-store
# 拉取源码并构建
sudo git clone --depth=1 https://github.com/sub-store-org/Sub-Store /opt/sub-store/repo
cd /opt/sub-store/repo/backend
sudo npm install -g pnpm@11
sudo pnpm install --frozen-lockfile
sudo pnpm bundle:esbuild
sudo chown -R substore:substore /opt/sub-store /var/lib/sub-store /etc/sub-store
3. 生成随机前缀和配置
PREFIX=$(openssl rand -hex 12)
sudo tee /etc/sub-store/sub-store.env >/dev/null <<EOF
SUB_STORE_BACKEND_API_HOST=127.0.0.1
SUB_STORE_BACKEND_API_PORT=3000
SUB_STORE_BACKEND_PREFIX=1
SUB_STORE_FRONTEND_BACKEND_PATH=/$PREFIX
SUB_STORE_DATA_BASE_PATH=/var/lib/sub-store
SUB_STORE_CORS_ALLOWED_ORIGINS=https://admin.bakersean.top,https://sub-store.vercel.app
EOF
sudo chown root:substore /etc/sub-store/sub-store.env
sudo chmod 640 /etc/sub-store/sub-store.env
[!warning] 安全要点
SUB_STORE_CORS_ALLOWED_ORIGINS一定不要保留默认值*,否则任何站点都能通过浏览器读取你的 API。
4. 配置 systemd 服务
[Unit]
Description=Sub-Store backend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=substore
Group=substore
WorkingDirectory=/opt/sub-store/repo/backend
EnvironmentFile=/etc/sub-store/sub-store.env
ExecStart=/usr/bin/node /opt/sub-store/repo/backend/sub-store.min.js
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/sub-store
[Install]
WantedBy=multi-user.target
5. 配置 Caddy
admin.bakersean.top {
encode zstd gzip
# Cloudflare Access JWT 校验(非 Access 认证的请求直接拒绝)
@no-auth {
not header CF-Access-JWT-Assertion *
}
respond @no-auth "Forbidden" 403
# 后端 API(随机前缀路径)
@backend path /<PREFIX>/*
handle @backend {
reverse_proxy 127.0.0.1:3000
}
# 根路径重定向,告知前端后端地址
@needs_api {
path /
not query api=*
}
redir @needs_api /?api=https://admin.bakersean.top/<PREFIX> 302
# 反向代理官方前端
reverse_proxy https://sub-store.vercel.app {
header_up Host sub-store.vercel.app
}
}
substore.bakersean.top {
encode zstd gzip
handle /share/* {
uri replace /share /<PREFIX>/share
reverse_proxy 127.0.0.1:3000
}
respond "Not Found" 404
}
6. UFW 防火墙:只允许 Cloudflare IP 回源
# Cloudflare IPv4 回源地址段
CF_IPS=(
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22
103.31.4.0/22 141.101.64.0/18 108.162.192.0/18
190.93.240.0/20 188.114.96.0/20 197.234.240.0/22
198.41.128.0/17 162.158.0.0/15 104.16.0.0/13
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22
)
for ip in "${CF_IPS[@]}"; do
sudo ufw allow from "$ip" to any port 80 proto tcp
sudo ufw allow from "$ip" to any port 443 proto tcp
done
# 删除旧的"Anywhere"规则(用 yes 管道自动确认)
while true; do
RULE=$(sudo ufw status numbered | grep -E '\b(80|443)/tcp\b.*Anywhere' | head -1)
[ -z "$RULE" ] && break
NUM=$(echo "$RULE" | grep -oP '^\[\s*\K\d+')
[ -n "$NUM" ] && yes | sudo ufw delete "$NUM" || break
done
sudo ufw allow 22/tcp
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw --force enable
[!tip] 为什么必须限制 Cloudflare IP 如果 80/443 对全网开放,任何人都能绕过 Cloudflare Access 直连服务器。UFW 限制回源 IP 是 Cloudflare Access 方案的”最后一公里”保障。
7. 配置 Cloudflare Access
在 Cloudflare Zero Trust 面板中创建两个 Self-hosted Application:
应用 1:管理前端(邮箱认证)
| 字段 | 值 |
|---|---|
| Application name | substore |
| Public hostname | admin.bakersean.top(无路径) |
| Policy Action | Allow |
| Include 规则 | Emails → 你的邮箱地址 |
| Session Duration | 24 hours 或 1 week |
应用 2:后端 API(Bypass 放行)
| 字段 | 值 |
|---|---|
| Application name | admin |
| Public hostname | admin.bakersean.top + Path: /<PREFIX>/* |
| Policy Action | Bypass |
| Include 规则 | Everyone |
[!important] 为什么需要两个应用 Cloudflare Access 的 Policy Include 规则中没有 Path 选项,路径匹配是在 Public hostname 层级定义的。因此需要为 API 路径单独创建一个应用,设置 Bypass 策略,否则后端 API 请求也会被 Access 拦截(表现为订阅导入返回登录页 HTML)。
调试记录:Basic Auth 认证循环弹窗
现象
访问 admin.bakersean.top 时,浏览器弹出原生 HTTP Basic Auth 对话框,输入用户名密码后立刻再次弹出,反复循环无法进入。
原因分析
- Caddy 配置了
basicauth *,所有路径(包括前端页面和后端 API)都要求 Basic Auth - 用户认证通过后,Sub-Store 前端 JS 加载,立刻向后端 API 发起
fetch/XHR 请求 - 关键:
fetch请求默认不会自动携带 Basic Auth 的Authorization头(与浏览器原生页面请求不同) - 后端 API 返回 401 → 浏览器再次弹出认证对话框 → 循环
根本原因:HTTP Basic Auth 是为传统”整页请求”设计的,不适合保护前后端分离架构中 JS 动态调用的 API。
解决方案
移除 Caddy 的 basicauth,改用 Cloudflare Access 保护管理面。Cloudflare Access 基于 cookie/session 工作,浏览器原生支持,不存在 JS API 调用丢凭据的问题。
常见问题
Q: Cloudflare Access 的 Tunnel 步骤要跳过吗?
是的。如果使用 Cloudflare DNS 代理回源(而非 cloudflared tunnel),在 Access 应用创建向导中点击 “Skip this for now” 跳过 Tunnel 分配步骤。
Q: Bypass 策略的 Include 里没有 Path 选项怎么办?
Cloudflare Access Policy 的 Include 规则不支持路径匹配。路径区分在 Application 的 Public hostname 层级配置——为需要 Bypass 的路径单独创建一个 Application。
Q: 订阅导入返回 Cloudflare 登录页 HTML?
说明后端 API 路径也被 Cloudflare Access 保护了。检查是否为 API 路径创建了 Bypass Application,以及该 Application 的 Public hostname 路径是否正确匹配 /<PREFIX>/*。
Q: Caddy 版本太低没有 jwt 指令怎么办?
Caddy 2.8+ 才内置 jwt 指令。低版本可以用 @no-auth { not header CF-Access-JWT-Assertion * } 检查头的存在性,配合 UFW 限制 Cloudflare IP 回源来实现等效保护。
验证清单
-
curl -I https://admin.bakersean.top返回 Cloudflare Access 登录页(未认证状态) - 通过邮箱验证后正常进入 Sub-Store 管理界面
-
curl https://admin.bakersean.top/<PREFIX>/api/file/all-nodes-all-ways直接返回订阅数据(Bypass 生效) -
curl -H "Host: admin.bakersean.top" http://<SERVER_IP>返回 403(UFW 拒绝非 Cloudflare IP) -
sudo ufw status确认 80/443 仅允许 Cloudflare IP 段 -
sudo systemctl status sub-store caddy确认服务正常运行