跳到主要內容
AnyStorage
免費下載

S3 錯誤碼

MinIO SignatureDoesNotMatch:七個原因與修法

MinIO 回報 SignatureDoesNotMatch 的實際原因只有七個:密鑰、時鐘偏移、區域、定址方式、反向代理、鍵名編碼、被改動的預簽章 URL。

MinIO 說 SignatureDoesNotMatch 時究竟在說什麼、怎麼和旁邊幾個容易混淆的錯誤分開,以及七個原因各自的檢查動作。

signaturedoesnotmatch minio,MinIO 簽章不符,MinIO 區域不一致

先看回應原文

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.ClientErrorS3Error、日誌裡一個乾巴巴的 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_USERMINIO_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」,並建議使用 ntptimedatectltimesyncd

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-1ap-northeast-1 對同一個請求會算出不同簽章。現行設定是 MINIO_SITE_REGIONmc admin config set ALIAS site region="us-east-1"),MINIO_REGIONMINIO_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 這邊有三個值:pathvirtualauto,文件寫的是「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 發出的原文
錯誤碼MinIO 發出的說明原文真正告訴你的事
InvalidAccessKeyIdThe Access Key Id you provided does not exist in our records.key ID 錯,或連錯伺服器
RequestTimeTooSkewedThe difference between the request time and the server's time is too large.是時鐘,不是憑證
AuthorizationHeaderMalformedThe authorization header is malformed; the region is wrong; expecting 'us-east-1'.區域不一致,還附上答案
InvalidRegionRegion does not match.同類,更簡短
XAmzContentSHA256MismatchThe provided 'x-amz-content-sha256' header does not match what was computed.請求內容在途中變了
AccessDeniedRequest 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 區域。只要兩端一致,換成別的區域同樣可用。

下一步