v3.1.17 或更高版本通过 CloudFlare Tunnel 访问 MinIO S3 提示 SignatureMismatch 问题的一些研究

我的服务器

群晖 Container Manager 部署 Docker 版 MinIO,没有公网 IP 所以使用 cloudflared docker(Tunnel)做内网穿透。

问题描述

思源笔记更新到 3.1.17 或更高版本之后,通过上述环境访问 MinIO 上的 S3 资源会提示认证失败:服务器计算得到的签名和客户端提供的不一致。

image.png

尝试解决问题时了解到的信息

能力有限,如果有错误的情报还请帮忙指出。

  1. 思源笔记从 3.1.17 版本开始使用 aws-sdk-go-v2 和后台 s3 服务进行认证。
    这个文件 的 blame 记录可以了解到思源使用的认证渠道的差异,但具体的认证方法还是要看亚马逊的源码。
  2. 不管是 3.1.16 或之前的版本,还是 3.1.17 或之后的版本,使用的认证规格都是 AWS Signature Version 4,前后的差异主要在于 aws-sdk-go-v2 的认证信息里,增加了一些 Header 要校验的内容,具体差异如下(粗体为新增的):
    • 旧版:
      • host
      • x-amz-content-sha256
      • x-amz-date
      • Authorization
    • go-v2:
      • accept-encoding
      • amz-sdk-invocation-id
      • amz-sdk-request
      • host
      • x-amz-content-sha256
      • x-amz-date
      • Authorization
  3. v4 的认证流程,据我所知是这样的(以 go-v2 版本的获取 BucketList API 为例):
    1. 客户端组装 request 的内容,包括上面提到的所有必要的 header,每一项的内容大致是:
      • accept-encoding:固定为 gzip

      • amz-sdk-invocation-id:具体的意思我不太清楚,但好像不太重要,原因之后解释

      • amz-sdk-request:我这里是 attempt=1; max=3

      • host:客户端认为的 s3 endpoint 地址,其实就是思源云端设置里填的网址(不包含 https:///后面的内容

      • x-amz-content-sha256:固定为 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

      • x-amz-date:当前日期和时间精确到秒,比如 20241231T133058Z

      • Authorization:最关键的信息,包含认证的基础信息,和上面 header 的顺序和组织方式,以及客户端计算得到的 Signature。以空格和逗号划分为四个部分:

        • AWS4-HMAC-SHA256:固定,表示签名认证版本为 v4。
        • Credential=5FF2U4ONuunfHGXxxxxx/20241231/us-east-1/s3/aws4_request:基础认证,提供了 Access Key、当前日期精确到天、目标桶的地区编号、固定 s3、固定 aws4_request。
        • SignedHeaders=accept-encoding;amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date:签名所需的 headers。
        • Signature=3050177a7243c268a3a3a21b982241898e1433f9a6d4abd4b2cde6e934xxxxxx:客户端计算得到的签名值。(计算方法见下文)
    2. 请求使用 https,通过 CloudFlare Tunnel 到达我在内网搭建的 MinIO 服务器。
    3. 在我这里,更新到 3.1.17 或更高版本之后,服务器会提示服务器计算的签名和客户端计算的不匹配,客户端显示报错信息。
  4. 签名不一致的原因,应该是上面提到的 Headers 或者 Host 在通过 CF Tunnel 的转发时发生了篡改(论坛上有人说 CF 的 Proxy 小黄云关闭之后就正常了,侧面印证了中间人篡改了某些数据),为了定位具体哪个数据被改,我简单看了下源码,也用 CF Workers 修改 Header 做了些试验,分 cai 析 ce 如下:
    • 根据报错提示信息,我遇到的问题就是单纯的签名计算问题,与地区的填写没有关系(至少还没到检验地区不匹配的步骤),我主要关注签名的计算方式和中间人的转发过程。
    • 签名的计算方式: 上面提到的所有 Header 字符串值(包括 Authorization 中 SignedHeaders 这一段),拼接成一个长字符串(可能是用\n),之后用 Secret Key 进行加密,加密方式为 AWS4-HMAC-SHA256,就得到了 Signature 值。
    • CloudFlare Tunnel 的转发流程:根据我的试验,CF 可能传改的信息只可能是 host 和 accept-encoding,因为其他条目我用 Worker 试过是可以任意修改且生效的(当然我自己修改也会导致签名失败,但从 CloudFlare Tunnel 的访问记录看确实可以修改成功),唯独这两条我是改不了的,严格说 Hader 里只有 accept-encoding 没法修改,host 我可以在 Tunnel 的设置里填写固定值所以问题不是它(经过我的试验这个设置真实有效,我在思源 3.1.16 中填写 worker 的 url 可以成功让 Tunnel 以为自己的 host 是 worker 的 url 而不是 Worker 转发时提供的,让签名通过验证)。另外,在 CloudFlare Tunnel 的访问记录中,accept-encoding 的值固定为 gzip, br,这与我在源码中观察到的情况(应该是只有 gzip)不符。
    • 综上,我认为问题的源头是 CloudFlare Tunnel 的服务篡改了 accept-encoding Header 的值,从而导致服务器提取 Header 信息计算得到的 Signature 和客户端发出的不符。

我的一些的失败尝试,以及感想

  1. 亚马逊的这个认证方式够严谨的,很大程度上规避了中间人篡改信息的风险。
    • 我曾经尝试在 Worker 里创建了一个新的 Header Accept-Encoding2 = gzip,然后篡改 Authorization 中的 accept-encoding->accept-encoding2,但是结果还是签名匹配失败。
    • 除了 host 因为 worker 的原因不匹配的问题(我后来在 Tunnel 设置里强制修改 host 成功解决了此问题)之外,我才发现 Authorization 的一部分竟然也被用来计算签名了,这下彻底捣毁了我想用 Worker 篡改数据曲线修改 accept-encoding 的诡计。
  2. 我尝试寻找 CloudFlare 关于 accept-encoding 内容的设置,目前没有收获。
  3. 既然 CloudFlare 不让修改压缩格式,我认为使用 Tunnel 的方式基本无解了,如果说还有什么解法的话那就要修改思源的源码了。
    这篇 Issue 提到的方法也许可以解决此问题,原理是使用反射之类的方法让签名 headers 名称列表里 ignore 掉 accept-encoding。
    我不太了解 go 和思源的代码怎么改,有缘人看到可以了解一下,看看是不是有必要尝试一下。
  4. 我最初希望内网搭建 S3 服务的初衷是想着在内网同步会非常快,但在外网无法访问这点我是无法接受的,因此目前的解决办法是将数据迁移到了 CloudFlare R2,因为是免费的,未来也许对比下访问速度,可能会再迁移到阿里云或者七牛云吧。
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    25442 引用 • 105225 回帖
1 操作
Whacka 在 2025-01-14 09:07:10 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 很有探索精神,不知道能不能给 cf 那边提提意见,应该很多人会遇到的,国外也是。亚马逊那个更新没多久。

    1 回复
    我看到那篇 Issue 是 22 年 8 月的
    Whacka
  • awatar

    亚马孙更新版本估计就是针对 CDN 这种情况,毕竟中间经过代理是有风险的,安全嘛 😭

    1 回复
  • 嗯,还是让 minio 去兼容好点。或者还是别折腾这个代理了。

  • ivistang 1 评论

    目前我找到一个可能可行的解决方案,仅供参考。大致思路如下:

    1. CF 的 cache rules 对 minio 的网站绕过缓存
    2. 配置请求头修改规则,把请求的 accept-encoding 值备份到其他头字段,比如 origin-accept-encodingimage.png
      这样就确保了数据存在
    3. 把 minio 的应用用 nginx proxy_pass 包起来,以便于修改请求头。
    4. 修改 nginx 配置,通过 proxy_set_header 修改请求头image.png
    5. 使用 mc admin trace -v myminio 检查请求头是否符合预期

    大功告成

    本质要解决三个问题:1. CF 的缓存会修改请求类型,必须避免缓存。2. accept-encoding 会被 cf 覆写所以必须在请求阶段备份数据。3. real_ip 也会影响 minio 的响应,使用真实 ip 而非 cf 的服务器 ip。 CF 的规则设置没法覆写 accept-encoding 这样的特殊字段,必须要用 nginx 反代。 楼主如果有兴趣折腾,可以试一下,我目前没遇到问题了。
    ivistang