安全地将 Cloudflare Worker 连接到自托管的源数据库服务器。
将数据库暴露在公共互联网是一个安全风险。恶意行为者会在互联网上搜索开放的数据库端口,并尝试使用暴力破解用户名/密码来获取您的敏感数据或专有数据。
通过阻止数据库的互联网访问,并使用 Cloudflare Worker 与数据库之间的安全隧道,可以降低这种风险。
这个项目是对 Cloudflare Workers 团队 worker-mysql 仓库的改进工作版本,我之前无法使用 Cloudflare Tunnel 和 Cloudflare Access 来保护它。
这个项目在 Docker 容器中本地运行 Cloudflare Tunnel(cloudflared 服务)和 MySQL 数据库。这些组件也可以在远程服务器上运行。通过对 PostgreSQL 数据库驱动程序进行相同的修改,这个示例可以支持 PostgreSQL 数据库。
Cloudflare Tunnel 现已免费。
CF_CLIENT_ID、CF_CLIENT_SECRET)
使用 Docker 在本地运行 cloudflared 和 MySQL。
如果使用 Mac 或 PC,Docker Desktop 是一个不错的选择。
使用 Node 版本管理器 来运行正确版本的 Node。
运行 nvm install 在本地开发环境中安装所需版本的 Node。
然后在每个终端窗口中运行 nvm use 来使用所需版本的 Node。
您需要在 Cloudflare 仪表板 中设置 Cloudflare Site/Website。按照这些设置说明进行操作。
本示例使用 example.com 域名。请替换为您自己的域名。
通过 Cloudflare UI,在 Cloudflare Worker 和 Docker 化 MySQL 数据库之间创建隧道。
MySQL 将在 Docker 容器内运行,因此从 cloudflared 的角度来看,MySQL 主机名/IP 是 host.docker.internal,即 Docker VM 的 IP。MySQL 端口是 3306。
注意,如果您不是在 Docker 中运行 MySQL,请使用服务: tcp://127.0.0.1:3306。
忽略关于 DNS 条目不存在的警告。这将自动在该域上创建一个 DNS CNAME,指向您的 Tunnel。

您新创建的隧道状态此时不会显示为 "Active"。只有在下一步设置并启动 cloudflared 后,它才会变为活动状态。
参考文档:
在 CLI 中,登录 Cloudflare 以授权 Cloudflare Tunnel。系统会要求您打开浏览器中的链接。运行 npm run cloudflared:login
运行此命令会生成证书并复制到 /home/nonroot/.cloudflared/cert.pem。在此示例中,您不需要移动证书,但如果以后想在远程服务器上使用此隧道,可能需要将其复制到远程服务器。
cloudflared 服务需要在您自己的服务器专用网络中运行。在大多数情况下,cloudflared 会与数据库服务器运行在同一服务器上,但也可以在专用网络内的独立服务器上运行。在此示例中,cloudflared 将与 MySQL 一起在本地开发计算机(127.0.0.1,host.docker.internal)上的 Docker 容器中运行。
重要说明:如果您在 Docker 中运行 cloudflared 但不是在 Docker 中运行 MySQL,请使用服务: tcp://127.0.0.1:3306。默认情况下,Docker 容器无法向主机(127.0.0.1)端口发出出站网络调用——因此 Cloudflare 无法连接到在主机(127.0.0.1)上运行的 MySQL。通过在 docker-compose.yml 文件中添加 network_mode: host 来设置 Cloudflared 与主机在同一网络上运行(请参阅注释掉的行)。
修改 docker-compose.yml 环境变量,添加来自 Cloudflare Tunnel 仪表板的 Cloudflare Tunnel 令牌:
TUNNEL_TOKEN=""
您可以在 Cloudflare Zero Trust 仪表板的 "Access" > "Tunnels" 下找到令牌:

一旦 TUNNEL_TOKEN 被填充,就可以使用 npm run cloudflared 启动 cloudflared Docker 容器。这也会启动 MySQL 容器,因为它们在 docker-compose.yml 中已关联。
参考文档:
服务令牌用于保护隧道免受公众访问。服务令牌存储在 Cloudflare Worker 和 Cloudflare 应用程序中,我们将在下一步进行设置。
在 Cloudflare Zero Trust 仪表板的 "Access" > "Service Auth" 下创建服务令牌:
点击 "Create Service Token",提供有意义的名称(例如 "db-tunnel-dev-service-token")和到期日期。

您将获得 CF_CLIENT_ID 和 CF_CLIENT_SECRET。复制密钥并将其作为敏感密码处理。
这些将需要您的 Cloudflare Worker 用于在 Cloudflare Tunnel 上进行身份验证。
通过限制谁可以访问您的 Cloudflare Tunnel 主机名来保护隧道免受恶意行为者攻击。
在 Cloudflare Zero Trust 仪表板的 "Access" > "Applications" 下创建应用程序:
步骤 1:

步骤 2:

通过 Cloudflare Tunnel 的所有通信在通过隧道通信时都需要在请求头中包含服务令牌:
CF-Access-Client-Id: e9dbd31ad.....98bf458b4.access
CF-Access-Client-Secret: 09725548845e2edaea70.......27e03a3785e39c59a50
此设置确保 MySQL 客户端通过 Cloudflare Tunnel 与源服务器(您的本地 Docker 化 MySQL)通信时发送上述头。MySQL 客户端已将上述头附加到每个请求。
CF-Access-Client-Id 和 CF-Access-Client-Secret 分别包含环境变量 CF_CLIENT_ID 和 CF_CLIENT_SECRET 的值。这些环境变量是在上面生成 Cloudflare 服务令牌时创建的。
虽然 CF_CLIENT_ID 和 CF_CLIENT_SECRET 可以以纯文本形式添加到 ./wrangler.toml 中,但更安全的方法是通过命令行将这些值持久化到 Wrangler Secrets 中:
npx wrangler secret put CF_CLIENT_ID
npx wrangler secret put CF_CLIENT_SECRET
当在 Secrets 环境变量中持久化时,这些值在发布时将自动可供您的 Workers 使用。
npm installdb-tunnel-dev.example.com。协议是 "https",而不是 "tcp":
[vars]
TUNNEL_HOST = "https://db-tunnel-dev.example.com"
TUNNEL_TOKEN。npm run dev如果所有设置都正确,在浏览器中访问 https://db-tunnel-dev.example.com 应该会返回 Cloudflare Access 401 Forbidden 页面。这是因为您没有使用有效的 Cloudflare 服务令牌(CF-Access-Client-Id 和 CF-Access-Client-Secret 头)发出请求。
从浏览器请求公共数据库端点(https://db-tunnel-dev.example.com),不使用服务令牌,将导致请求被阻止(太好了!):

上述步骤已建立了安全隧道,提供了一个进入您专用网络的安全门口。请确保您的 MySQL 数据库位于您的专用网络中,而不是可直接在互联网上访问的公共网络中。
在本地开发计算机(或服务器)上实施防火墙,确保 MySQL 的 3306 端口不能从互联网访问。如果可以通过互联网访问,它就会暴露给恶意行为者,需要保护。
如果您的 MySQL 数据库可以直接通过互联网访问,那么建立通向它的隧道是没有意义的。
cloudflared 和 MySQL Docker 容器:npm run cloudflared。npm run dev错误: CF_CLIENT_ID 未定义。 尝试使用全局变量访问绑定(模块)。 您必须使用导出处理器/持久对象构造函数的第二个 env 参数,或使用 Pages Functions 的 context.env。
使用模块 Worker 时,不要使用 globalThis 变量。
此限制在 Miniflare 的这个提交中引入。
在此博客文章中了解服务 Worker 和模块 Worker 之间的区别。
// 模块 Worker 格式
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
globalThis.CF_CLIENT_ID = env.CF_CLIENT_ID || undefined
console.log(globalThis.CF_CLIENT_ID) // <-- 会报错!
}
}
// 模块 Worker 格式
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
console.log(env.CF_CLIENT_ID) // 直接使用 `env` 并在应用中传递
}
}
✘ [错误] 在配置的目标环境("es2020")中顶层 await 不可用。
原始的 worker-mysql 不再支持顶层 await(TLA)。TLA 需要被包装。
await setup(DEFAULT_CONFIG);
(async () => {
await setup(DEFAULT_CONFIG);
})();
127.0.0.1
cloudflared-tunnel | 2022-11-09T16:43:47Z 错误 error="dial tcp 127.0.0.1:3306: connect: connection refused" cfRay=767802675b1472e2-LHR ingressRule=0 originService=tcp://127.0.0.1:3306
或
localhost
cloudflared-tunnel | 2022-11-09T16:43:47Z 错误 error="dial tcp localhost:3306: connect: connection refused" cfRay=767802675b1472e2-LHR ingressRule=0 originService=tcp://localhost:3306
当 cloudflared 和 MySQL 都在 Docker 容器内运行时,cloudflared 连接 MySQL 容器的 IP 地址是 Docker 的内部 IP地址,可通过主机名 host.docker.internal 访问。
确保隧道的公共主机名指向 cloudflared 可访问的 IP/主机名。在 Cloudflare Zero Trust 仪表板的 "Access" > "Tunnels" > 您的隧道 > "Public Hostname" 下。


错误 请求失败 error="dial tcp 127.0.0.1:3306: connect: connection refused" connIndex=0 dest=<主机名> ip=<IP 地址> type=ws
默认情况下,Docker 容器无法向主机(127.0.0.1)端口发出出站网络调用,因此 Cloudflare 无法连接到在主机上运行的 MySQL。通过在 docker-compose.yml 文件中添加 network_mode: host 来设置 Cloudflared 与主机在同一网络上运行(请参阅注释掉的行)。
欢迎通过 PR 和提交 issue 做出贡献。