Skip to content

Fixing R2 SignatureDoesNotMatch on presigned URLs

Covers the URL being altered after signing (re-encoding, reordering, or stripping of query parameters) and signing with a stale or wrong secret key. Does not cover expired presigned URLs, which R2 reports as a separate error code, ExpiredRequest — check that first, since the fix is completely different (just regenerate the URL).

Unverifiedno reproductions yetWhy this confidence?
Revision 1published by DevYou curationrevision history
Run the diagnosisEvidence and compatibility

Symptoms

The diagnostic path

8 steps. Every step is written out below in full — the interactive version simply follows the branches for you.

  1. Step 1 · StartPresigned R2 URL fails with SignatureDoesNotMatch

    R2 returns HTTP 403 with S3 error code SignatureDoesNotMatch (R2 error code 10035): the request signature it calculated does not match the one in the URL. Note this is a different error code from ExpiredRequest (10018) — if the code is ExpiredRequest instead, the URL simply expired and needs regenerating; that's not what this playbook covers.

    What happens next

    • passed step 2, Check whether the URL was modified in transit
  2. Step 2 · TestCheck whether the URL was modified in transit

    Compare the exact URL you generated against the one the client actually requested. Proxies, chat apps generating link previews, email clients, and even some HTTP libraries can re-encode or reorder query parameters, which invalidates the signature since it covers the exact query string.

    Read-onlysh
    diff <(echo "$GENERATED_URL") <(echo "$RECEIVED_URL")

    Expected result

    A difference in the query string — e.g. %2F decoded to /, or parameters in a different order — between the generated and received URLs.

    What happens next

    • passed step 3, Keep the URL byte-for-byte unmodified end-to-end
    • failed step 4, Check whether the signing key still matches
    • unknown step 3, Keep the URL byte-for-byte unmodified end-to-end
  3. Step 3 · FixKeep the URL byte-for-byte unmodified end-to-end

    Don't let anything between signing and use touch the URL — no manual re-encoding, no passing it through a query-string-stripping redirect or shortener. If it has to pass through infrastructure that might mangle it, sign it server-side immediately before handing it to the client rather than caching a previously-generated URL.

    What happens next

    • passed step 5, Generate a fresh URL and use it immediately, unmodified
  4. Step 4 · TestCheck whether the signing key still matches

    Confirm the access key ID / secret access key your Worker is signing with is the one currently active for that R2 API token — a rotated secret or a mismatched key pair (from a different token) produces exactly this error.

    Read-onlysh
    npx wrangler secret list

    Expected result

    The secret last updated around the same time signing started failing, or a key pair that doesn't correspond to an active R2 API token in the dashboard.

    What happens next

    • passed step 6, Update the secret used for signing
    • failed step 7, Not a URL-mangling or key-rotation issue — escalate
    • unknown step 7, Not a URL-mangling or key-rotation issue — escalate
  5. Step 5 · Verify the fixGenerate a fresh URL and use it immediately, unmodified

    Generate a new presigned URL and request it right away with curl, without passing it through anything that might alter it.

    Read-onlysh
    curl -X PUT "$FRESH_PRESIGNED_URL" --data-binary @test-file

    Expected result

    A 200 (or 2xx) response, with no SignatureDoesNotMatch.

    What happens next

    • passed step 8, Resolved
    • failed step 7, Not a URL-mangling or key-rotation issue — escalate
    • unknown step 7, Not a URL-mangling or key-rotation issue — escalate
  6. Step 6 · FixUpdate the secret used for signing

    Set the current, active secret access key as the Worker secret, and make sure both the access key ID and secret access key you construct AwsClient/S3Client with come from the same, current R2 API token.

    Handles credentialssh

    Touches credentials or secrets. Never paste real secrets into a shared terminal.

    Replaces the secret value used to sign new presigned URLs. Any presigned URL already issued under the old secret stops validating as soon as this deploys, so links already sent to users may break before they're used.

    npx wrangler secret put R2_SECRET_ACCESS_KEY

    What happens next

    • passed step 5, Generate a fresh URL and use it immediately, unmodified
  7. Step 7 · EndNot a URL-mangling or key-rotation issue — escalate

    Check whether a header included in signing (most commonly Content-Type) doesn't match what the actual request sends — R2's presigned signature covers whichever headers were included at sign time, and a client sending a different Content-Type than the one signed will also produce SignatureDoesNotMatch.

  8. Step 8 · EndResolved

    The presigned URL now validates because it reaches R2 unmodified and is signed with the current key.

Sources

Why this confidence?

What would strengthen it: 6 more independent reproductions. Reproductions from 3 more distinct environments.