Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
GCP with AI By James Joyner IV · · 8 min read Last reviewed Jul 2026

GCP Error Guide: 'SignatureDoesNotMatch' on Cloud Storage — Fix Signed URL Errors

Quick answer

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
Free toolkit

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 SignatureDoesNotMatch on 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, added x-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 GET but used with PUT, 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-url to 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 ExpiredToken distinctly from SignatureDoesNotMatch in 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.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.