AWS

SignatureDoesNotMatch: The request signature we calculated does not match

The signature AWS computed differs from yours. Almost always a clock, a wrong region, a stray whitespace in a key, or a proxy rewriting headers.

medium fix5 min read

the aws error
An error occurred (SignatureDoesNotMatch) when calling the ListObjectsV2 operation: The request signature we calculated does not match the signature you provided. Check your key and signing method.

<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided.</Message></Error>

An error occurred (AuthorizationHeaderMalformed) when calling the GetObject operation: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'eu-west-1'

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Check the clock first, it costs nothing

    timedatectl status 2>/dev/null | head -5 || date -u

    Signature Version 4 includes a timestamp and AWS rejects anything more than 15 minutes out. A suspended laptop or a VM restored from a snapshot is the usual source, and the error gives no hint that time is involved.

  2. 2

    Look for whitespace or quoting damage in the secret key

    aws configure get aws_secret_access_key | od -c | tail -3

    od shows a trailing newline or space that is invisible in an editor. A key pasted from a wrapped terminal, or set in a YAML file without quoting, is a very common cause.

  3. 3

    Confirm the region matches the bucket

    aws s3api get-bucket-location --bucket acme-assets

    SigV4 binds the signature to a region. Signing for us-east-1 against a bucket in eu-west-1 produces AuthorizationHeaderMalformed, which names both regions and is the clearest variant of this error.

All 9 sections

The signature you sent does not match the one AWS computed from the same request. Because SigV4 signs the method, path, query string, several headers, the payload hash, the timestamp, the region and the service, anything that alters one of those on the way produces this.

It is rarely a wrong secret key, though that is where people look first.

Check the clock

timedatectl status | head -5
date -u

SigV4 includes X-Amz-Date, and AWS rejects requests more than 15 minutes from its own clock. No part of the error mentions time, so this is easy to miss.

sudo systemctl restart systemd-timesyncd
sudo chronyc makestep      # if using chrony

Laptops resumed from long suspends, VMs restored from snapshots, and containers on a host with a drifting clock are the usual sources. Inside a container the clock comes from the host, so fix it there.

Look for whitespace in the key

aws configure get aws_secret_access_key | od -c | tail -3
0000060   /   9   K   D   e   x   a   m   p   l   e   K   E   Y  \n

A trailing space or newline is invisible in an editor and breaks the HMAC. Common sources:

  • Pasting from a terminal that wrapped the line
  • A CI secret saved with a trailing newline
  • YAML without quotes, where trailing spaces survive
  • export AWS_SECRET_ACCESS_KEY=$(cat key.txt) where the file ends with a newline
export AWS_SECRET_ACCESS_KEY="$(tr -d '[:space:]' < key.txt)"

Special characters matter too. A secret key containing / or + inside an unquoted shell variable, or embedded in a URL, gets mangled.

Confirm the region

aws s3api get-bucket-location --bucket acme-assets
{ "LocationConstraint": "eu-west-1" }

A null result means us-east-1, which is a quirk worth knowing.

Signing for the wrong region gives a more specific message:

AuthorizationHeaderMalformed: the region 'us-east-1' is wrong; expecting 'eu-west-1'

That variant is the friendly one, because it names both. Set the region explicitly:

aws s3 ls s3://acme-assets --region eu-west-1

A proxy rewriting the request

Any middlebox that modifies signed headers breaks the signature:

  • A corporate proxy adding or normalising headers
  • A TLS-inspecting proxy
  • An API gateway or WAF in front of an S3-compatible endpoint
  • A load balancer rewriting Host

Host is always signed, so anything that changes it invalidates the signature. Content-Type is frequently signed too.

env | grep -i proxy
curl -sv https://acme-assets.s3.eu-west-1.amazonaws.com/ 2>&1 | grep -i "^< "

Test with the proxy bypassed:

no_proxy="s3.eu-west-1.amazonaws.com" aws s3 ls s3://acme-assets

Presigned URLs

aws s3 presign s3://acme-assets/report.pdf --expires-in 3600

Things that break a presigned URL:

Expiry. Maximum is 7 days for SigV4, and 12 hours when signed with a role's temporary credentials, because the URL cannot outlive the session that created it.

The credential expired. A presigned URL created with a role session dies when that session does, regardless of the expiry you asked for.

Extra query parameters. Anything appended after signing, including analytics parameters added by a link shortener or an email tracker, invalidates the signature.

Re-encoding. A URL passed through something that re-encodes %2F or + in the signature breaks it. Presigned URLs should be treated as opaque.

Uploads with a changed body

For PutObject, the payload hash is signed. Anything that alters the body in transit breaks it:

  • A proxy compressing or decompressing
  • Reading from a stream that was partially consumed before signing
  • A Content-Length that disagrees with what is sent

For large uploads, use multipart, which signs each part independently and is more robust as well as resumable.

S3-compatible endpoints

MinIO, Cloudflare R2, Backblaze B2 and similar implement SigV4 with differences. Two settings usually matter:

aws configure set default.s3.addressing_style path
aws --endpoint-url https://minio.internal:9000 s3 ls

Virtual-hosted addressing puts the bucket in the hostname, which changes the signed Host header. Many self-hosted endpoints only support path style.

Some also require a specific region string, often us-east-1, regardless of where they run.

A checklist

  1. Check the clock. date -u against real time. 15 minute tolerance.
  2. od -c the secret key for a trailing newline or space.
  3. get-bucket-location and sign for the bucket's real region.
  4. AuthorizationHeaderMalformed names both regions; that is the easy variant.
  5. Check for a proxy. Host is always signed.
  6. Presigned URL → check expiry, and whether the signing session has ended.
  7. Never append query parameters to a presigned URL.
  8. S3-compatible endpoint → try addressing_style = path.

Frequently Asked Questions

Why does a valid secret key produce SignatureDoesNotMatch?

Because the signature covers far more than the key: the HTTP method, the canonical path and query string, several headers including Host, a hash of the payload, the timestamp, the region and the service. Anything that differs between what you signed and what AWS received produces a mismatch. In practice the most common causes are a clock more than 15 minutes out, invisible whitespace on the end of the key, the wrong region, and a proxy rewriting headers in transit.

How much clock drift does AWS tolerate?

Fifteen minutes in either direction. Beyond that the request is rejected, and nothing in the error message mentions time, which makes it a frustrating thing to chase. Check with date -u against a known-good source and restart your NTP service. Machines that suspend for long periods, virtual machines restored from snapshots, and containers inheriting a drifted host clock are the usual offenders; inside a container you must fix the host.

Why does my presigned URL stop working before it expires?

Most likely because it was signed with temporary credentials from an assumed role, and a presigned URL cannot outlive the session that created it. A URL requested with a twelve hour expiry but signed by a one hour role session dies after an hour. The other common cause is something appending query parameters after signing, such as a link shortener or an email tracker adding analytics, since every query parameter is part of the signed request.

What does AuthorizationHeaderMalformed mean?

It is the region variant of the same problem, and it is the most helpful version because it names both the region you signed for and the one the bucket is in. SigV4 binds a signature to a specific region, so signing for us-east-1 against a bucket in eu-west-1 cannot validate. Run aws s3api get-bucket-location to get the real region, remembering that a null result means us-east-1.

Why does this happen against MinIO or R2 but not AWS?

Because S3-compatible endpoints implement SigV4 with small differences, most often around addressing style. AWS defaults to virtual-hosted addressing, which puts the bucket name in the hostname and therefore in the signed Host header, while many self-hosted endpoints only support path-style addressing. Set aws configure set default.s3.addressing_style path. Some implementations also require a particular region string, commonly us-east-1, regardless of where they actually run.

Reference and practice

Learn the underlying concept

Other AWS errors