logo
0
0
WeChat Login

Cloudflare Worker 与自托管 MySQL 连接示例

目的

安全地将 Cloudflare Worker 连接到自托管的源数据库服务器。

简介

将数据库暴露在公共互联网是一个安全风险。恶意行为者会在互联网上搜索开放的数据库端口,并尝试使用暴力破解用户名/密码来获取您的敏感数据或专有数据。

通过阻止数据库的互联网访问,并使用 Cloudflare Worker 与数据库之间的安全隧道,可以降低这种风险。

这个项目是对 Cloudflare Workers 团队 worker-mysql 仓库的改进工作版本,我之前无法使用 Cloudflare Tunnel 和 Cloudflare Access 来保护它。

这个项目在 Docker 容器中本地运行 Cloudflare Tunnel(cloudflared 服务)和 MySQL 数据库。这些组件也可以在远程服务器上运行。通过对 PostgreSQL 数据库驱动程序进行相同的修改,这个示例可以支持 PostgreSQL 数据库。

Cloudflare Tunnel 现已免费

功能特性

  • Wrangler 2.0
  • TypeScript
  • Cloudflare Worker
  • 支持本地开发
  • 自托管 MySQL 数据库(远程或本地)
  • 使用 Docker 化 Cloudflared 服务的 Cloudflare Tunnel
  • 使用 Cloudflare Access 服务令牌保护 Cloudflare Tunnel 的 Cloudflare 应用程序
  • 带有 Cloudflare Access 服务令牌的 MySQL 客户端/驱动程序(CF_CLIENT_IDCF_CLIENT_SECRET
  • 实现新的 ES 模块/模块 Worker(而非服务 Worker)
  • 敏感环境变量

组件图

alt text

前置条件

1. Docker

使用 Docker 在本地运行 cloudflared 和 MySQL。

如果使用 Mac 或 PC,Docker Desktop 是一个不错的选择。

2. Node 版本管理器

使用 Node 版本管理器 来运行正确版本的 Node。

运行 nvm install 在本地开发环境中安装所需版本的 Node。

然后在每个终端窗口中运行 nvm use 来使用所需版本的 Node。

3. Cloudflare 网站

您需要在 Cloudflare 仪表板 中设置 Cloudflare Site/Website。按照这些设置说明进行操作。

设置

本示例使用 example.com 域名。请替换为您自己的域名。

1. Cloudflare Tunnel

通过 Cloudflare UI,在 Cloudflare Worker 和 Docker 化 MySQL 数据库之间创建隧道。

MySQL 将在 Docker 容器内运行,因此从 cloudflared 的角度来看,MySQL 主机名/IP 是 host.docker.internal,即 Docker VM 的 IP。MySQL 端口是 3306

  • 访问 Cloudflare Zero Trust 仪表板,这与标准仪表板不同。
  • 选择 "Access" > "Tunnels"。
  • 点击 "Create a Tunnel" 按钮。
  • 提供一个描述性名称,例如 "Database Tunnel - Dev"。
  • 提供一个公共主机名,例如 "db-tunnel-dev.example.com"
    • 子域名,例如 "db-tunnel-dev"
    • 域名 - 从下拉列表中选择您的 Cloudflare 网站,例如 "example.com"
    • 路径 - 留空
    • 服务,例如 "tcp://host.docker.internal:3306"
      • 类型: "tcp"
      • URL: "host.docker.internal:3306"

注意,如果您不是在 Docker 中运行 MySQL,请使用服务: tcp://127.0.0.1:3306

忽略关于 DNS 条目不存在的警告。这将自动在该域上创建一个 DNS CNAME,指向您的 Tunnel。

alt text

您新创建的隧道状态此时不会显示为 "Active"。只有在下一步设置并启动 cloudflared 后,它才会变为活动状态。

参考文档:

2. 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" 下找到令牌:

alt text

一旦 TUNNEL_TOKEN 被填充,就可以使用 npm run cloudflared 启动 cloudflared Docker 容器。这也会启动 MySQL 容器,因为它们在 docker-compose.yml 中已关联。

参考文档:

3. Cloudflare 服务令牌

服务令牌用于保护隧道免受公众访问。服务令牌存储在 Cloudflare Worker 和 Cloudflare 应用程序中,我们将在下一步进行设置。

Cloudflare Zero Trust 仪表板的 "Access" > "Service Auth" 下创建服务令牌:

点击 "Create Service Token",提供有意义的名称(例如 "db-tunnel-dev-service-token")和到期日期。

alt text

您将获得 CF_CLIENT_IDCF_CLIENT_SECRET。复制密钥并将其作为敏感密码处理。

这些将需要您的 Cloudflare Worker 用于在 Cloudflare Tunnel 上进行身份验证。

4. Cloudflare 应用程序

通过限制谁可以访问您的 Cloudflare Tunnel 主机名来保护隧道免受恶意行为者攻击。

Cloudflare Zero Trust 仪表板的 "Access" > "Applications" 下创建应用程序:

  • 步骤 1:
    • 点击 "Create an Application" 按钮。
    • 选择 "Self-Hosted" 作为您要隧道传输的应用程序类型。
    • 应用程序名称,例如 "db-tunnel-dev-app"
    • 应用程序域名,例如 "db-tunnel-dev.example.com"
      • 子域名,例如 "db-tunnel.dev"
      • 域名,例如 "example.com" - 您的 Cloudflare 网站下拉列表。
  • 步骤 2:
    • 策略名称,例如 "db-tunnel-dev-policy"
    • 操作: "Service Auth"
    • 配置规则:
      • 包含选择器: "Service Token"
      • 包含值(服务令牌),例如 "db-tunnel-dev-service-token" - 上一步创建的服务令牌。

步骤 1: alt text

步骤 2: alt text

5. Cloudflare Worker

通过 Cloudflare Tunnel 的所有通信在通过隧道通信时都需要在请求头中包含服务令牌:

CF-Access-Client-Id: e9dbd31ad.....98bf458b4.access
CF-Access-Client-Secret: 09725548845e2edaea70.......27e03a3785e39c59a50

此设置确保 MySQL 客户端通过 Cloudflare Tunnel 与源服务器(您的本地 Docker 化 MySQL)通信时发送上述头。MySQL 客户端已将上述头附加到每个请求。

CF-Access-Client-IdCF-Access-Client-Secret 分别包含环境变量 CF_CLIENT_IDCF_CLIENT_SECRET 的值。这些环境变量是在上面生成 Cloudflare 服务令牌时创建的。

虽然 CF_CLIENT_IDCF_CLIENT_SECRET 可以以纯文本形式添加到 ./wrangler.toml 中,但更安全的方法是通过命令行将这些值持久化到 Wrangler Secrets 中:

npx wrangler secret put CF_CLIENT_ID
npx wrangler secret put CF_CLIENT_SECRET

当在 Secrets 环境变量中持久化时,这些值在发布时将自动可供您的 Workers 使用。

6. 运行示例 Cloudflare Worker

  • npm install
  • wrangler.toml 中将 Tunnel 主机更新为之前定义的 Cloudflare Tunnel 主机名,例如 db-tunnel-dev.example.com。协议是 "https",而不是 "tcp":
    [vars]
    TUNNEL_HOST = "https://db-tunnel-dev.example.com"
    
  • 确保 docker-compose.yml 包含您之前步骤中的 TUNNEL_TOKEN
  • npm run dev

如果所有设置都正确,在浏览器中访问 https://db-tunnel-dev.example.com 应该会返回 Cloudflare Access 401 Forbidden 页面。这是因为您没有使用有效的 Cloudflare 服务令牌(CF-Access-Client-IdCF-Access-Client-Secret 头)发出请求。

从浏览器请求公共数据库端点(https://db-tunnel-dev.example.com),不使用服务令牌,将导致请求被阻止(太好了!):

alt text

7. 保护您的自托管数据库

上述步骤已建立了安全隧道,提供了一个进入您专用网络的安全门口。请确保您的 MySQL 数据库位于您的专用网络中,而不是可直接在互联网上访问的公共网络中。

在本地开发计算机(或服务器)上实施防火墙,确保 MySQL 的 3306 端口不能从互联网访问。如果可以通过互联网访问,它就会暴露给恶意行为者,需要保护。

如果您的 MySQL 数据库可以直接通过互联网访问,那么建立通向它的隧道是没有意义的。

运行

  1. 启动 cloudflared 和 MySQL Docker 容器:npm run cloudflared
  2. 使用 Miniflare 在本地运行 Cloudflare Worker:npm run dev
  3. 访问 http://localhost:8787

故障排除

问题使用 globalThis

错误: 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` 并在应用中传递
  }
}    

顶层 Await

✘ [错误] 在配置的目标环境("es2020")中顶层 await 不可用。

原始的 worker-mysql 不再支持顶层 await(TLA)。TLA 需要被包装。

不要这样做

await setup(DEFAULT_CONFIG);

正确做法

(async () => {
  await setup(DEFAULT_CONFIG);
})();

Localhost vs Docker

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" 下。

不要这样做

alt text

正确做法

alt text

无法将 Cloudflared 连接到 MySQL(不在 Docker 容器中)

错误日志

错误 请求失败 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 做出贡献。

About

No description, topics, or website provided.
Language
JavaScript96.3%
TypeScript3.3%
Python0.4%