Minio + Cloudflare Tunnel 踩坑记:解决 Obsidian Remotely Save 插件同步 403 错误


一、 背景与问题

近期,我计划将 S3 存储迁移至家中 NAS 上运行的 Docker 版 Minio,主要目的是为了让 Obsidian 的 Remotely Save 插件能将笔记同步到这个私有的对象存储上。

在配置过程中,我遇到了一个奇怪的 403 Forbidden 错误。它的奇特之处在于:

  • 检查通过Remotely Save 插件的“检查”功能(Check)每次都能成功。

  • 同步失败:但真正执行“同步”(Sync)操作时,就会稳定地报 403 错误。

我的网络环境采用了内外网分离的 DNS 解析策略(Split-horizon DNS),以实现最佳访问路径:

网络架构设计

  • 内网访问:当手机、电脑等设备连接家庭 Wi-Fi 时,通过内部 DNS 将域名 minio-api.tomyail.com 解析为 NAS 的内网 IP。流量直接在局域网内传输,速度快、延迟低。

  • 外网访问:当设备在外部网络时,域名 minio-api.tomyail.com 会被公网 DNS 解析,流量通过 Cloudflare Tunnel 穿透到家中的 Kubernetes 集群,再由集群路由至 NAS。

这种差异化的访问路径,也导致了内外网需要不同的解决方案。

二、 Minio 部署配置 (Docker Compose)

Minio 服务本身部署在 NAS 的 Docker 环境中,配置如下:

YAML

version: '3'
services:
minio:
image: bitnami/minio:2025.4.22
restart: always
ports:
- "9011:9001" # MinIO 控制台端口
- "9010:9000" # MinIO API 端口
environment:
# 设置 MinIO 的根用户和密码。请务必将它们更改为强密码!
- MINIO_ROOT_USER=xxx
- MINIO_ROOT_PASSWORD=xxx
- MINIO_BROWSER=on
volumes:
- ./data:/bitnami/minio/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3

💡 关于 Minio 版本的重要提示

较新版本的 Minio 官方镜像 (minio/minio) 移除了 Web 控制台的大部分管理功能(如 IAM 策略、存储桶管理等)。为了保留完整的 UI 功能,可以选择一个较早的、功能未被移除的版本。

  • 官方镜像推荐版本: minio/minio:RELEASE.2025-04-22T22-12-26Z

  • Bitnami 镜像对应版本: bitnami/minio is 2025.4.22

三、 方案一:内网访问 (Nginx Proxy Manager)

在内网,我使用 Nginx Proxy Manager (NPM) 来管理所有服务的自定义域名和 SSL 证书。为了让 Minio 在反向代理后正常工作,必须在 NPM 的高级配置中添加以下 Nginx 指令:

# 确保后端 Minio 服务能收到正确的 Host 头,这是签名验证成功的关键
proxy_set_header Host $http_host;
# 传递真实的客户端 IP 地址
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递客户端原始的协议类型 (http/https)
proxy_set_header X-Forwarded-Proto $scheme;
# 为 Minio 控制台可能用到的 WebSocket 提供支持
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
# 解除上传文件大小的限制
client_max_body_size 0;

添加这些配置后,内网通过 minio-api.tomyail.com 访问 Minio 一切正常。

四、 方案二:外网访问 (Kubernetes + Cloudflare Tunnel)

外网访问的链路更长,流量经由 Cloudflare Tunnel 进入我家中的 Talos Kubernetes 集群,再由集群内的 Gateway API 路由到 NAS。

为了让集群能访问到集群外部的 NAS 上的 Minio 服务,我创建了 EndpointsService 资源,手动将服务指向了 NAS 的 IP 地址。

# 1. 创建 Endpoints,手动指定 Minio 服务的后端 IP 和端口
apiVersion: v1
kind: Endpoints
metadata:
name: cold-minio-api
subsets:
- addresses:
- ip: "192.168.50.220" # NAS 的内网 IP
ports:
- port: 9010 # Docker 映射出的 API 端口
---
# 2. 创建 Service,使其能够被集群内部的其他服务发现
apiVersion: v1
kind: Service
metadata:
name: cold-minio-api
spec:
ports:
- port: 9010
targetPort: 9010
---
# 3. 创建 HTTPRoute,通过 Gateway 将域名流量路由到该服务
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cold-minio-api-route
spec:
parentRefs:
- name: external
namespace: kube-system
sectionName: https
hostnames:
- "minio-api.tomyail.com"
rules:
- backendRefs:
- name: cold-minio-api
port: 9010

五、 最终的“罪魁祸首”与解决方案

即使配置了上述路由,外网访问在“同步”时依然失败,稳定复现 403 错误。

经过一番排查,我发现问题根源并非出在 Kubernetes 的路由层(Cilium Gateway API),而是出在了最前端的 Cloudflare

核心原因:Cloudflare 的代理服务(橙色云朵)并不仅仅是简单地转发流量。它会主动地对请求进行分析、改写和缓存。这种“智能”行为对于静态网站是极大的优化,但对于像 S3 这样对请求格式有严格规范的 API 来说,却是致命的。任何未经预期的修改都可能导致 Minio 服务端的签名验证失败。

例如,社区中就有用户报告,在开启“全部缓存”(Cache Everything) 时,Cloudflare 会将 HEAD 请求转为 GET 请求,这完美解释了为什么我的“检查”(通常是 HEADList 请求)能通过,而“同步”(PUT 请求)会失败。

最终解决方案:配置 Cloudflare 缓存规则

解决办法非常直接:告诉 Cloudflare “请不要碰我的 API 流量”。

  1. 登录 Cloudflare 控制台,选择您的域名。

  2. 进入左侧菜单的 缓存 (Caching) -> 缓存规则 (Cache Rules)

  3. 创建一条新规则,配置如下:

    • 字段 (Field): 主机名 (Hostname)

    • 运算符 (Operator): 等于 (equals)

    • 值 (Value): minio-api.tomyail.com

    • 选择缓存资格 (Cache eligibility): 选择 绕过缓存 (Bypass cache)

./1.png 这条规则强制 Cloudflare 对所有发往 minio-api.tomyail.com 的请求完全不进行任何缓存和可能的内容改写,而是直接、透明地将原始请求转发到后端。

配置完成后,外网同步的 403 问题迎刃而解。