先看返回体原文
xml
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
</Error>这句 Message 不是转述,它就是 MinIO 服务端为这个错误码准备的原文,HTTP 状态码是 403。各家 SDK 包装方式不同——botocore.exceptions.ClientError、S3Error、日志里一个干巴巴的 403——但服务端对所有人说的是同一句话。
它的含义很窄,也因此很有用:访问密钥 ID 找到了,请求解析成功了,然后 MinIO 用它实际收到的内容重算了 Signature Version 4,结果和你送来的签名不一样。所以该问的从来不是"我密码是不是错了",而是"服务端看到了什么我没发出去的东西"。能改变这件事的因素有七个。
| 你观察到的现象 | 原因 | 修法 |
|---|---|---|
| 所有客户端都立刻失败 | Secret key 错,或末尾多了空格、换行 | 从没有换行的文件重新设置凭据 |
| 一台机器能用,另一台不行 | 时钟偏差超过 15 分钟 | 同步时间;运气好会直接报 RequestTimeTooSkewed |
us-east-1 能用,自己的区域不行 | 凭据作用域里的区域和服务端不一致 | 对齐 MINIO_SITE_REGION |
GET 正常、PUT 失败;或把桶名放进主机名就失败 | 对 path-style 服务端用了虚拟主机寻址 | 强制 path 寻址 |
| 直连 9000 端口正常,走 nginx 就失败 | 代理改写了 Host 等参与签名的头 | 原样透传 Host |
只有带空格、+、非 ASCII 的键失败 | 路径在链路中被重新编码 | 别让代理动路径 |
| 手动改过预签名 URL 之后就失败 | 签名之后的任何改动都作废 | 重新生成,不要追加查询参数 |
1. 先排除 key ID,再怀疑 secret
MinIO 对 key ID 错误有单独的错误码:InvalidAccessKeyId,原文是「The Access Key Id you provided does not exist in our records.」。既然你看到的是 SignatureDoesNotMatch,说明 ID 是对得上的,坏掉的是另一半,或者签名过程。
常见元凶是复制粘贴事故:一个换行、一个尾随空格、被 shell 吃掉的 $。把 alias 明确写一遍,只跑一条命令验证:
sh
mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
mc ls local/MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD 就是 MinIO 读取初始管理员凭据的环境变量名,另有 MINIO_ROOT_USER_FILE / MINIO_ROOT_PASSWORD_FILE 从密钥文件读取——后者更值得养成习惯,因为文件彻底绕开了 shell 引号问题。
2. 动别的之前,先看时钟
SigV4 把时间戳放进了签名内容,时钟不对签名就不对。MinIO 允许的窗口是固定的,源码里写着 globalMaxSkewTime = 15 * time.Minute // 15 minutes skew allowed.。它的软件清单对集群也是同一个数字:「All servers in a distributed deployment must have clocks synchronized to within 15 minutes of each other」,并建议用 ntp、timedatectl 或 timesyncd。
sh
date -u # 客户端
timedatectl status | head -5 # 服务端MinIO 自己识别出偏差时,会返回 RequestTimeTooSkewed——「The difference between the request time and the server's time is too large.」——这属于比较幸运的结局。时钟停住的容器、刚从睡眠恢复的笔记本,也可能刚好落在 15 分钟以内,却仍然过不了签名。
3. 对齐区域,或者干脆不发
区域是 SigV4 凭据作用域的一部分,所以 us-east-1 和 cn-north-1 对同一个请求会算出不同签名。现行配置项是 MINIO_SITE_REGION(mc admin config set ALIAS site region="us-east-1"),MINIO_REGION 与 MINIO_REGION_NAME 在源码里已标为 legacy。
能判断出来的时候 MinIO 有专门的抱怨:AuthorizationHeaderMalformed,「The authorization header is malformed; the region is wrong; expecting 'us-east-1'.」。期望值就写在消息里,照抄给客户端即可。
4. path-style 与虚拟主机寻址
裸装的 MinIO 响应 http://host:9000/bucket/key。只有设置了 MINIO_DOMAIN,它才会把桶名当作主机名的一部分——这个变量在源码里被解析成虚拟主机域名列表。让一个虚拟主机寻址的客户端去连没有该设置的服务端,客户端签名用的 host 和 MinIO 期望的 host 就不是一回事。
AWS CLI 这边有三个取值:path、virtual、auto,文档写的是「The default value in the CLI is to use auto, which will attempt to use virtual where possible, but will fall back to path style if necessary.」
sh
aws configure set default.s3.addressing_style path
aws --endpoint-url http://127.0.0.1:9000 s3 ls s3://my-bucket/5. 反向代理是惯犯
host 是参与签名的头。代理把它改写成 localhost、上游名或另一个端口,等于在客户端签完名之后改了签名内容,MinIO 自然算出别的签名。nginx 里关键的一行是 proxy_set_header Host $http_host;,它转发原始头,而不是替换成上游名字。
另外两行来自 MinIO 的负载均衡指引:client_max_body_size 0;(允许任意大小上传),以及 proxy_request_buffering off; 与 proxy_buffering off;。缓冲本身不破坏签名,但它会在同一个下午把大文件传输弄坏,然后让签名背锅。
带空格或 + 的键只在个别情况下失败,属于同一类问题:链路里只要有人对路径做归一化或二次解码,canonical request 就对不上了。先用纯 ASCII、不带空格的键测一次,如果它通了而怪键不通,路径就是在途中被改写的。
6. 预签名 URL 一经签名就被冻住
预签名 URL 带着 X-Amz-SignedHeaders,签名覆盖方法、路径、查询和有效期。追加一个参数、把 http 换成 https、改端口、对路径重新编码,都会让它失效;用已经轮换掉的凭据签出来的也一样。
过期是另一句话:错误码 AccessDenied,说明文字是「Request has expired」。所以预签名 URL 报 SignatureDoesNotMatch,意味着 URL 被改写过——短链服务、邮件客户端、往链接上加跟踪参数的 CDN 都是常客。
读懂旁边那几个错误
| 错误码 | MinIO 发出的说明原文 | 真正告诉你的事 |
|---|---|---|
InvalidAccessKeyId | The Access Key Id you provided does not exist in our records. | key ID 错,或连错了服务端 |
RequestTimeTooSkewed | The difference between the request time and the server's time is too large. | 是时钟,不是凭据 |
AuthorizationHeaderMalformed | The authorization header is malformed; the region is wrong; expecting 'us-east-1'. | 区域不一致,还附带答案 |
InvalidRegion | Region does not match. | 同类,更简短 |
XAmzContentSHA256Mismatch | The provided 'x-amz-content-sha256' header does not match what was computed. | 请求体在途中变了 |
AccessDenied | Request has expired | 预签名 URL 过期 |
把签名这件事交给客户端
这正是让客户端替你做 SigV4 运算的理由。AnyStorage(v0.2.24;macOS 11+、Windows 10+、Ubuntu 20.04+)可以连接任何 S3 兼容端点,包括 MinIO:端点 URL、access key、secret key,并把 path-style 寻址和自定义区域做成显式选项——上面的原因 3 和原因 4 说的正是这两个开关。免费版可用两个连接。挂载走的是内置的本地 WebDAV 服务(http://127.0.0.1:3211),链路里没有 FUSE、WinFsp 或 macFUSE。
为什么 curl 能通,SDK 不行?
因为两者签的东西不一样。SDK 会加上 x-amz-content-sha256、校验和、内容类型等头,其中任何一个都可能出现在 SignedHeaders 里。打开 SDK 的调试日志,把它的 canonical request 和你手签的那条比一比。
MinIO 日志里没有有用信息,该看哪里?
响应体就是日志。保留原始 XML,而不是 SDK 的异常文本:里面的错误码能区分六种失败,而异常文本会把它们压成一种。
桶策略写错也会报这个吗?
不会。授权失败以 AccessDenied(说明为「Access Denied.」)返回。签名校验发生在任何策略评估之前。
us-east-1 有什么特殊?
只是惯例。MinIO 的报错里提到它,是因为它是最常见的默认值;Cloudflare R2 也文档化了空值与 us-east-1 都等价于它的 auto 区域。只要两端一致,换成别的区域同样能用。
下一步
- MinIO 的桌面客户端:MinIO GUI 客户端
- 浏览器侧的同类战斗:R2 CORS 错误
- 生成不会坏掉的链接:S3 预签名 URL 生成器