GCP Error Guide: 'SignatureDoesNotMatch' on Cloud Storage — Fix Signed URL Errors
Fix Cloud Storage 'SignatureDoesNotMatch' on signed URLs: align the signing key, canonical headers, expiry, and clock so computed and provided signatures agree.
- #gcp
- #cloud
- #troubleshooting
- #errors
Stuck on this GCP with AI error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
Cloud Storage rejects a request whose computed signature does not match the one you supplied, typically on a V4 signed URL or an HMAC-authenticated request. The literal XML response reads:
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
</Error>
An expired signed URL returns a closely related error you should not confuse with it:
<Error><Code>ExpiredToken</Code><Message>The provided token has expired.</Message></Error>
SignatureDoesNotMatch means the server recomputed the signature over the request it actually received and got a different value than the client sent. The cause is always a mismatch between what was signed and what was sent — a wrong key, a changed header, an altered path, or a clock/expiry problem.
Symptoms
403 SignatureDoesNotMatchon GET/PUT to a signed URL that “worked yesterday” or works from one client but not another.- Uploads via signed URL fail when the client adds or omits a header (e.g.
Content-Type) the signature didn’t cover. - HMAC-signed S3-compatible requests to GCS fail with the same code.
- Intermittent failures tied to a service account key rotation.
- Works with one signing method/library but not a hand-rolled one.
Common Root Causes
- Wrong or rotated signing key — the private key or HMAC secret used to sign differs from the one GCS expects (often after a key rotation).
- Header mismatch — a header included in the signed canonical request differs from what the client actually sent (extra
Content-Type, addedx-goog-*, or a proxy injecting headers). - Altered path or query — the object name, bucket, or query parameters were re-encoded or changed between signing and sending (double-encoding, trailing slash, case).
- Clock skew / expiry — the signing host’s clock is off, so the request timestamp is outside the allowed window, or the expiry elapsed (
ExpiredToken). - Wrong signing region/service — for HMAC/S3-style requests, signing with the wrong region or service string.
- Method mismatch — the URL was signed for
GETbut used withPUT, or vice versa.
Diagnostic Workflow
Reproduce with a fresh, known-good signed URL from gcloud to isolate your generator vs. your request:
gcloud storage sign-url gs://BUCKET/OBJECT \
--private-key-file=KEYFILE.json \
--http-verb=GET --duration=10m
Test that URL exactly as issued (no added headers), then compare with your client:
curl -v "SIGNED_URL" # -v shows headers actually sent vs. what was signed
Confirm the signing service account and its key are current (rules out a rotated key):
gcloud iam service-accounts keys list --iam-account=SIGNER@PROJECT.iam.gserviceaccount.com
Check host clock skew on the signing machine — timestamp drift alone causes this error:
timedatectl status | grep -i 'System clock\|NTP'
Example Root Cause Analysis
An upload service issued V4 signed URLs for browser PUTs. Uploads worked in testing but failed in production with SignatureDoesNotMatch.
Diagnosis: a curl -v of the signed URL from a script succeeded, but the browser upload failed. Comparing headers showed the browser sent a Content-Type: image/png header, while the signing code signed the URL without including Content-Type in the canonical request. Because V4 signatures cover the signed headers exactly, the extra header the browser added changed the request the server saw, so the recomputed signature didn’t match.
Root cause: a header mismatch — the client sent a Content-Type that was not part of the signed canonical request, so the server’s recomputed signature differed from the one provided.
Fix: include Content-Type in the signed headers and require the client to send exactly that value (or sign without it and forbid the client from setting it). Once the signed and sent headers agreed, uploads succeeded. No key or permission change was needed.
Prevention Best Practices
- Use the official client libraries /
gcloud storage sign-urlto generate signed URLs rather than hand-rolling the V4 canonical string. - Sign exactly the headers the client will send — and no more; document which headers (especially
Content-Type) the client must include verbatim. - Keep signing hosts on NTP; even small clock skew invalidates timestamped signatures.
- Rotate service-account keys deliberately and update the signer atomically so in-flight URLs signed with the old key are accounted for.
- Choose realistic expiry durations and handle
ExpiredTokendistinctly fromSignatureDoesNotMatchin client code. - Never re-encode or rewrite the object path/query between signing and sending (watch for proxies and URL normalizers).
Quick Command Reference
# Generate a known-good signed URL to isolate the problem
gcloud storage sign-url gs://BUCKET/OBJECT --private-key-file=KEY.json --http-verb=GET --duration=10m
# Send exactly as issued and inspect actual headers
curl -v "SIGNED_URL"
# List the signer's keys (check for rotation)
gcloud iam service-accounts keys list --iam-account=SIGNER@PROJECT.iam.gserviceaccount.com
# Verify clock sync on the signing host
timedatectl status | grep -i NTP
Conclusion
SignatureDoesNotMatch on Cloud Storage means the server recomputed the signature over the request it actually received and got a different value than the client sent — so the fix is to make what you sign and what you send identical. The usual culprits are a rotated signing key, a header the client added or omitted (especially Content-Type), a re-encoded path, or clock skew. Reproduce with a known-good gcloud storage sign-url, diff the headers actually sent against those signed, and confirm the key and clock — the mismatch is always somewhere in that comparison.
Fixed it? Get 500 GCP with AI & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.