S3 SignatureDoesNotMatch: The 4 Causes, Cheapest First

Logeshwaran
—

SignatureDoesNotMatch from S3 means the request that arrived is not the request you signed. Work through four suspects, cheapest first: the clock on the machine that did the signing, the secret access key it actually used, the Region it signed for, and anything (a header, the HTTP method, a proxy) that changed after signing. Here is the twist that saves hours: this error is not about permissions. A valid key with full access to the bucket can still trigger it, while a key with no access at all gets a different error.

⚡ Quick Answer

• Clock → Sync the machine that signs the request. For authenticated requests the allowed gap is 15 minutes, and presigned URLs are less forgiving. On EC2, point chrony at 169.254.169.123. Steps

• Key → Console: IAM → Users → your user → Security credentials → Access keys, and confirm the key is Active. CLI: aws configure list shows which key is really in use. Steps

• Region → aws s3api get-bucket-location --bucket YOUR_BUCKET, then sign for that Region. Steps

• Request → Send exactly what you signed: same method, same headers, same Content-Type, same bucket and key spelling, nothing rewriting it on the way. Steps

If you only read this box: fix the clock first, then the key, then the Region, then the request. Each fix takes minutes, and none of them costs money.

Picture Jake's shop on a Saturday at 9 a.m. His photo-upload page lets customers send pictures of cracked screens before they walk in. Suddenly every upload comes back as a 403 with this text: The request signature we calculated does not match the signature you provided. Check your key and signing method. He does what most of us do. He creates a new access key. Then another. Same error.

Ethan: "Stop rotating keys, Jake. That message points at your key, but the same message covers a wrong clock, a wrong Region and a header that changed on the way. Think of a wax seal. You press the seal over the letter and the date. S3 receives the letter, presses its own seal, and compares. If the two seals differ, the letter is refused, and S3 does not tell you why."

That is the whole game, and it is why the fix is a short checklist rather than a mystery. This page covers the console route and the CLI route for each of the four causes, the presigned URL version of the same error, the look-alike errors that waste your afternoon, and what to gather if you end up opening a support case. If you only have a minute, the Quick Answer box above has all four checks.

What "the request signature we calculated does not match" actually means

The error arrives as an HTTP 403 Forbidden with the error code SignatureDoesNotMatch. Depending on the tool, you will see one of two wordings at the end of the message: Check your key and signing method or Check your AWS secret access key and signing method. Both are the same error. The full sentence people paste into search is The request signature we calculated does not match the signature you provided, and the CLI wraps it as An error occurred (SignatureDoesNotMatch) when calling the ... operation.

A signature is a short fingerprint your tool computes over the request before sending it. The AWS SDKs and the AWS CLI do this for you on every call. When the request reaches S3, S3 rebuilds the fingerprint from what actually arrived and compares. If the two differ by even one character, you get this error.

The fingerprint is built from several ingredients, and each of them belongs to one of our four causes. Your secret access key (the private half of an access key pair; the access key ID is the public half) is used to derive a signing key that is tied to one date, one Region and one service. The timestamp comes from your machine's clock. The credential scope is the string YYYYMMDD/region/service/aws4_request, and for S3 the service part is s3. The rest is the request itself: the method, the path, the query string, the signed headers and a hash of the body.

Ingredient in the signature Where it comes from Which cause
Timestamp (x-amz-date or X-Amz-Date) The clock on the machine that signs 1. Clock
Secret access key (and session token, if temporary) Whichever credentials the tool resolved 2. Key
Credential scope: date, Region, service The Region setting or endpoint in your code 3. Region
Method, path, query string, signed headers, body hash The request as your client built and sent it 4. Request

Notice what is missing from that table: permissions. A key that lacks permission is turned away with AccessDenied, and an access key ID that does not exist is turned away with InvalidAccessKeyId. Neither of those is today's error. This one means the seal did not match, which is why so many people burn an hour editing IAM policies that were never the problem.

One more piece of context. S3 no longer supports the older Signature Version 2, so every request has to be signed with Signature Version 4 (SigV4, the algorithm called AWS4-HMAC-SHA256). If you are running very old code, jump to the section on the signing method further down.

Start here: which of the four is yours?

Ethan asks one question before touching anything: "Did this exact request ever work from this exact machine?" The answer narrows the field faster than any log.

What you see Most likely cause Jump to
It worked before, code unchanged, and the machine was recently restored, moved, paused or rebuilt Clock Cause 1
It started right after you created, rotated or pasted a key; or the CLI works and your app does not Key Cause 2
One bucket works and another fails; a new bucket lives in a different Region; you set a custom endpoint Region Cause 3
A presigned URL opens in a browser but fails from your uploader, or fails only when you add a header Request Cause 4
It fails only from the office network or through a proxy Request Cause 4
A presigned URL works for a while and then stops Probably expiry, which is a different error Presigned URLs

If the table does not settle it, run this short triage. It costs five minutes and it usually points at one cause.

  1. Read the whole error body, not the one-line message. The XML that S3 returns includes a StringToSign block and request IDs, and you will want both later.
  2. Compare the signing machine's time in UTC against a trusted clock. A gap of more than a few minutes is your answer; go to Cause 1.
  3. Run aws configure list on the same machine and account that fails, and look at where the access key and secret key rows say they come from.
  4. Run aws s3api get-bucket-location --bucket YOUR_BUCKET and compare the answer with the Region your code signs for.
  5. Replay the failing request with curl -v and compare the method and headers you meant to send with the ones that actually went out.

🙋‍♂️ Jake's Reality Check

"The message literally says check your key. Why would I look anywhere else first?"

The straight answer. The key is one of four suspects, and it is the slowest one to change, because a new key has to be pasted into every place the old one lives. The clock and the Region take seconds to look at. Check the cheap things first.

Cause 1: The clock on the signing machine is wrong

Every signed request carries a timestamp, and S3 compares it with its own clock. As of September 2026, the timestamp on an authenticated request has to be within 15 minutes of S3's time. Outside that window S3 refuses the request with its own error code, RequestTimeTooSkewed (HTTP 403), whose message reads The difference between the request time and the server's time is too large. Fifteen minutes is 900 seconds, or 900,000 milliseconds.

So why does a clock problem sometimes show up as SignatureDoesNotMatch instead? For presigned URLs the rule is blunt: keep the clock synchronized with a Network Time Protocol (NTP) server, because even small drifts can invalidate signatures. NTP is the everyday protocol computers use to ask a trusted time server what time it is. I cannot promise which drift produces which error name, so do not treat 14 minutes as a safe margin. The goal is a clock that is right to within a second or two, not one that is merely inside the window.

The time zone trap

The timestamp in a signed request is written in UTC, in the format you see in X-Amz-Date=20260930T100000Z, where the trailing Z means UTC. A machine showing the correct local time with the wrong time zone selected is really off by whole hours in UTC. This is easy to miss because the clock in the corner of the screen looks perfect.

The midnight trap (custom signing code)

Two more date rules matter if you build signatures yourself. The timestamp must be in UTC, written as YYYYMMDDTHHMMSSZ, with no milliseconds. And the date inside the credential scope must match the date of the request. AWS rejects a mismatch even when the two are seconds apart: a timestamp of 20151014T235959Z with a scope dated 20151015 fails.

That means code that reads the clock once for the timestamp and again for the scope date can fail only when the two readings straddle midnight UTC. It looks random, because it is. Ethan: "A bug that only appears at midnight UTC, which is dinner for some of us and breakfast for others, is a bug that gets blamed on the wrong thing for weeks. Read the clock once." The SDKs and the CLI handle this for you, which is one more reason to use them.

Whose clock matters

The clock that matters is the one on the machine that signs. For a presigned URL, that is the server, Lambda function or laptop that generated the URL, not the visitor's phone. S3 checks the expiration date and time against its own clock at the moment of the request, so a customer with a wrong phone clock is not your problem. A developer laptop that generated the URL might be.

🙋‍♂️ Jake's Reality Check

"I keep the wall clock in my shop 20 minutes fast so I'm never late opening up. Can I do that to the laptop too?"

The straight answer. No. Twenty minutes is 1,200 seconds, which is past the 900-second window. Keep the wall clock fast if it gets you to work on time. Keep the laptop honest.

Fix it on an EC2 Linux instance

Amazon Linux 2023 and recent versions of Amazon Linux 2 already use the Amazon Time Sync Service by default, so if one of those is wrong, something changed the configuration. The service answers at 169.254.169.123 (IPv4) from any Region, and at fd00:ec2::123 (IPv6) on Nitro-based instances.

  1. Ask the system what it is syncing with: timedatectl timesync-status on a systemd machine, or chronyc sources if chrony is the time daemon. A line starting ^* beside 169.254.169.123 means it is locked on to the local service.
  2. If it is not, open /etc/chrony.conf and make sure it contains server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4.
  3. Restart the daemon, for example sudo service chronyd restart, and run chronyc tracking. The reference ID should read A9FEA97B (169.254.169.123).
  4. Retry the failing request. If it still fails with a healthy clock, this was not your cause; move on to the key.

Do not mix time sources that behave differently. A mix of smeared and non-smeared servers can produce a clock that hops around. If you are adding a second source, keep the local service line and add only the public one as a backup.

Fix it outside EC2 (a laptop, an office server, a home lab)

There is no S3 console setting for this, because the clock lives on the machine and not in your AWS account. On a laptop, turn on the operating system's automatic date, time and time zone settings and use its sync option. On a Linux server without the local service, the public Amazon Time Sync Service works from anywhere: add pool time.aws.com iburst to /etc/chrony.conf, then run sudo service chronyd force-reload.

# Linux: see what the clock is doing right now
chronyc tracking
chronyc sources

# Print the current time in UTC, the way S3 sees it
date -u +%Y%m%dT%H%M%SZ

On a Windows Server that lost its time settings, you can point the Windows time service at the local endpoint like this: stop the service with net stop w32time, run w32tm /config /syncfromflags:manual /manualpeerlist:"169.254.169.123", then w32tm /config /reliable:yes, then net start w32time. Windows AMIs on EC2 normally ship configured for the IPv4 endpoint already.

Ethan: "Two habits catch clock trouble before it becomes an outage. Suspect any machine that was asleep, snapshotted, restored or paused. And suspect the clock inside a container or virtual machine, not only the clock of the machine underneath it."

What it looks like when this is not the cause

If chronyc tracking shows a tiny offset and the error persists, stop tuning time. Move to the key. A useful sanity check before you do: sign one request from a second, known-good machine using the same credentials. If that one works, the problem is local to the first machine, which could still be its clock but is more often its credentials or its network path.

Cause 2: The secret key is wrong, stale, or not the one you think you're using

The secret access key has to be in one of three healthy states to sign anything. It cannot be incorrect, it cannot be invalid, and it cannot be turned off (an IAM access key can be set to Inactive, and a key in that state stops signing). Stray characters and extra spaces count as incorrect. One extra space at the end of a pasted secret is enough, and so is a line break that a chat app inserted for you.

The name of the error tells you which half of the pair is wrong. If the access key ID does not exist, you get InvalidAccessKeyId. If the ID is real but the secret does not belong with it, you get SignatureDoesNotMatch. That second case includes an ID from one key pair mixed with a secret from another, which is an easy mistake right after a rotation.

The override trap: environment variables beat your fresh credentials file

This is the cause that catches careful people. The AWS CLI resolves credentials in a fixed order: command-line options first, then environment variables, then the files in your home folder. If AWS_SECRET_ACCESS_KEY is set in your shell, it overrides the secret in your profile. The same is true of AWS_SESSION_TOKEN. So you can rotate a key, update ~/.aws/credentials, and still be signing with the old secret that a startup script exported months ago.

# Shows where each credential is coming from (values are masked)
aws configure list

# Is anything exported in this shell?
env | grep -E '^AWS_'

# Remove the overrides for this shell session
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

In the aws configure list output, look at the column beside the access key and secret key rows that says where each value comes from. If it says env, environment variables are supplying them; any other value names the file or profile that is. Whatever the column says is what signs your requests, regardless of what you believe you configured.

Temporary credentials need their session token

Credentials from an assumed role, IAM Identity Center or the Security Token Service (AWS STS, the service that hands out short-lived keys) come as three parts: an access key ID, a secret and a session token. Leave the token out, or send a token that belongs to a different session, and the signature will not match. If you refresh one part by hand and not the others, you have built exactly that mismatch. Temporary credentials also expire, and a role session lasts one hour by default when you use AssumeRole, so a script that ran fine all morning can fail at lunch.

Console route: rotate an IAM user's access key

  1. Sign in to the AWS Management Console and open the IAM console.
  2. Choose Users, choose the user your app signs with, then choose the Security credentials tab.
  3. In the Access keys section, choose Create access key. The new key is active by default, so the user now has two.
  4. Download the .csv file or copy the secret into your app's secret store right then, because that is the moment the console shows it to you.
  5. Restart or reload the app, and confirm the failing request works.
  6. Back on the same tab, find the old key, choose Actions, then Deactivate. Delete it after a few days of silence.

CLI route

# Re-enter the pair by hand for the default profile
aws configure

# Or for a named profile
aws configure --profile shop-uploads

# Any simple call proves the pair signs correctly
aws s3api get-bucket-location --bucket YOUR_BUCKET --profile shop-uploads

When you type the secret into aws configure, paste it carefully and check for a trailing space. If a secret keeps failing, create a fresh pair instead of hunting for the bad character. Creating a new pair costs nothing.

⚠️ What this actually breaks

Deleting the old key before every app has the new one turns a signature problem into a total outage. Deactivate first, wait, then delete. And never paste a secret access key into a chat, a ticket or a public repo. If you have to share a key, share only the access key ID, which is not secret.

✅ Why this is the one to use

If your code runs on EC2, Lambda or ECS, use a role and let the platform hand out credentials. My advice is to stop copying long-lived keys around at all, because most stale-key problems disappear when there is no key to go stale.

Cause 3: The signature was made for the wrong Region

Remember the credential scope from earlier, YYYYMMDD/region/service/aws4_request. The Region in that string is baked into the signature. If your code signs for us-east-1 and the bucket lives in us-west-2, the two seals cannot match. A presigned URL is a common victim, because the URL is generated once and used later, often by code that was never told which Region the bucket is in.

Jake named his bucket after the shop cat, Biscuit, then created it while the console was pointed at a different Region than his code. The cat is not to blame, though he did sit on the keyboard at the time.

Find the bucket's real Region

aws s3api get-bucket-location --bucket example-bucket

# Example output
# {
#     "LocationConstraint": "us-west-2"
# }

# List only the buckets in one Region
aws s3api list-buckets --region us-east-2 --bucket-region us-east-2

The ListBuckets call returns each bucket's name and its Region, which makes it the quickest way to audit an account with many buckets. In the console, open the S3 console and choose General purpose buckets to see the list; the CLI command above answers the Region question in one line.

Compare the answer with the Region your code signs for. Then compare it again, because the bucket may have been deleted and re-created in a different Region under the same name. A Region setting that was right last year can be stale now.

Set the Region explicitly, every time

The aws s3 presign command signs with SigV4, so the Region has to be configured explicitly, and here is a full example that also pins the endpoint:

aws s3 presign s3://amzn-s3-demo-bucket/mydoc.txt \
  --expires-in 604800 \
  --region af-south-1 \
  --endpoint-url https://s3.af-south-1.amazonaws.com

For boto3, the Python SDK, set the signature version, the bucket's Region and virtual-hosted addressing (where the bucket name is part of the host name) on the client before generating URLs:

import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    region_name="us-west-2",
    config=Config(signature_version="s3v4",
                  s3={"addressing_style": "virtual"}),
)

url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": "example-bucket", "Key": "photo.jpg"},
    ExpiresIn=3600,
)

Region errors that are not this error

S3 has its own Region complaints, and they read differently. If you see IllegalLocationConstraintException, you are reaching for a bucket from a different Region than the one it exists in, and the message tells you to use the --region option. IncorrectEndpoint says the bucket exists in another Region and to direct requests to the correct endpoint. AuthorizationHeaderMalformed says the authorization header is not valid. When a general SigV4 service (not only S3) sees a Region mismatch in the scope, its wording is Credential should be scoped to a valid Region, not region-code. Same family, different symptom, same fix: sign for the bucket's Region.

Cause 4: The request changed after it was signed

This is the sneakiest bucket, Ethan says. A signature covers the exact request. If the method, the bucket or key name, a signed header or the query string differs between signing and sending, S3 builds a different seal from what arrived. Your key can be perfect and your clock exact, and it still fails.

What went wrong Example Fix
Wrong HTTP method URL signed for GET, uploader sends PUT Generate and use the URL for the same method
Header not signed Client adds a header that was not part of the signature Drop the header, or include it when generating the URL
Header value differs image/JPEG signed, image/jpeg sent Match the value exactly, including case
Missing Content-Type ContentType was set when the URL was generated, but not sent Send the same Content-Type header
Wrong bucket or key Photos/a.jpg signed, photos/a.jpg requested Match the name, including case
Something rewrote the request A corporate proxy edits headers or the query string Test without the proxy; ask the network admin to stop rewriting

Presigned uploads and Content-Type

A presigned URL contains only the host, the path and the query parameters. Headers are not in it. So if you generated the URL with a ContentType, the client that uses it has to send the same Content-Type header. Sending a different value, or none, is the classic reason a presigned PUT fails with this error when a plain upload worked.

curl -X PUT -T "abcd-video.mp4" -H "Content-Type: video/mp4" "YOUR_PRESIGNED_URL"

The reverse also fails. If you send a header that was not part of the signature, the request will not match. If you intend to send headers with a presigned URL, they have to be accounted for when the URL is generated. Be careful with tools that add headers on their own. A browser, a mobile SDK or a testing tool such as Postman may attach a Content-Type you never asked for; look at the request that actually left, not the one you meant.

HEAD is a different method

Ethan's favorite trap: curl -I sends a HEAD request. A URL signed for GET will fail when tested that way, and people conclude the URL is broken. Test a GET URL with a GET.

A related edge case has a different error name but the same root: when Range is one of the signed headers, S3 requires If-Range to be signed too, if the request contains it. The error is AccessDenied with HeadersNotSigned: if-range, and the fix is to add If-Range to X-Amz-SignedHeaders when you generate the URL.

Proxies, firewalls and anything else in the middle

Some corporate proxies modify headers or query strings. If a request fails only from one network, test the same request without the proxy. The same idea covers anything acting like a proxy: a gateway, a security appliance, a client library handler that attaches extra signed headers. A request that is edited after signing does not match the seal.

Ethan: "A presigned URL is a promissory note for one exact request, Jake. It does not say 'upload something', it says 'this method, this file, these headers'. Deliver anything else and the bank says no."

If you sign requests by hand

If you are writing your own signing code instead of using an SDK, the most likely culprit is a difference in the canonical request or the string to sign. The error response includes the string S3 built, so compare it, character by character, with yours. Remember that AWS reads the timestamp from x-amz-date first and only falls back to the Date header if that is missing, so a header you set may not be the one that counts. Then use an SDK or the CLI to make a known-good request with the same credentials, which separates a bad key from bad signing code. My advice: use the SDK. Signature calculation is an area where small mistakes are very hard to see.

Presigned URLs: the same four causes, plus a deadline

A presigned URL is a link that carries its own signature, so someone without AWS credentials can download or upload one specific object for a limited time. It is a very common place to meet SignatureDoesNotMatch, because the signing and the using happen at different times, on different machines, sometimes by different code. All four causes apply. There is a fifth wrinkle: the link can die before the time you typed. As of September 2026, the same presigned URL rules also cover the S3 annotation operations, such as PutObjectAnnotation and GetObjectAnnotation.

A presigned URL only works while the credentials that made it are valid. It expires at its configured time or when those credentials expire, whichever comes first.

Credentials that signed it How long the URL can live (as of September 2026)
IAM user, SigV4 Up to 7 days (604,800 seconds) from the CLI or an SDK
Made in the S3 console 1 minute to 12 hours (43,200 seconds)
IAM role or STS credentials Ends when the role session ends, even if you asked for longer; the default AssumeRole session is 1 hour
EC2 instance profile The life of the role credentials, typically up to about 6 hours
ECS task or container role Credentials typically rotate every 1 to 6 hours, and the URL goes with them

Two rules apply to all of them. First, S3 checks the expiration at the moment of the request, so a big download that starts before the deadline keeps going after it passes, but a resume after the deadline fails. Second, a link made from temporary credentials dies with them. Jake's customers expect a link to work instantly and forever. S3 can do instantly, and forever tops out at seven days.

Expiry is a different error

If the URL simply ran out of time, S3 returns AccessDenied with Request has expired, not SignatureDoesNotMatch. If the credentials behind it lapsed, expect an ExpiredToken error (HTTP 400). Both point at the same fix: generate a new URL with fresh credentials. If you are seeing this page's error and not an expiry message, the URL is being rejected for a reason other than age.

Make the URL, console and CLI

In the S3 console, open the bucket under General purpose buckets, select the object and use the console's presigned URL option, setting a duration from 1 minute to 12 hours. From the CLI the default lifetime is one hour:

aws s3 presign s3://YOUR_BUCKET/photo.jpg --region YOUR_REGION

# One hour is the default; this asks for one week (the maximum)
aws s3 presign s3://YOUR_BUCKET/photo.jpg --expires-in 604800 --region YOUR_REGION

A bucket can shorten the life of a URL

A bucket policy can deny presigned requests whose signature is too old, using the s3:signatureAge condition, measured in milliseconds. A value of 600000 is 10 minutes. If someone on your team added a rule like that, a URL you thought would last a day would stop after ten minutes, and the response would be a denial, not a signature error.

⚠️ What this actually breaks

A presigned URL is a bearer token: whoever holds it can use it, with the permissions of the person who made it. Do not paste a full URL into a chat or a ticket. Remove the X-Amz-Signature part first.

A worked example: one URL, three clocks

Let's put real numbers on it. Suppose a function that Jake's booking page calls generates a presigned PUT URL on September 30, 2026 at 10:00:00 UTC. The URL carries X-Amz-Date=20260930T100000Z and X-Amz-Expires=3600. On paper it is good until 11:00:00 UTC. But the function runs on a role session that started at 09:40:00 UTC and, at the default one hour, ends at 10:40:00 UTC.

Moment (UTC) What is true What to expect
10:00:00 URL signed; 3,600 seconds requested Nothing to see yet
10:35:00 Session has 5 minutes left Works, if the four causes are clean
10:40:00 Role session ends URL stops working, 20 minutes before its printed deadline
10:45:00 Customer clicks An expiry-style error, such as Request has expired or ExpiredToken, not this page's error
11:00:00 Printed deadline Irrelevant, the session already ended

Now add a clock problem. Say a developer generates the same URL on a laptop whose clock runs 16 minutes fast, so it stamps 20260930T101600Z when real UTC is 10:00:00. Sixteen minutes is 960 seconds, which is 60 seconds past the 900-second window for authenticated requests. Even a drift of 12 minutes (720 seconds) would sit inside that window, but for presigned URLs the rule is to keep the clock synced because small drifts can invalidate signatures. So the safe reading is: any drift is a suspect, and a big one is nearly a certainty.

The lesson in numbers: the URL had a deadline of 60 minutes on paper, 40 minutes in practice, and it was signed with a laptop clock that made it worthless from the start. Three different clocks, three different failures, and only one of them is the error in this article's title.

If you need a URL to last, the fix is to sign with credentials that last: an IAM user's key (up to 7 days) rather than a role session that ends in an hour. My advice is the opposite for most apps: keep the URLs short-lived, generate them on demand, and let the app ask for a new one.

Look-alike errors that send you down the wrong road

Half the value of reading the error name is ruling things out. These are the neighbors, with their real HTTP status codes, so you do not spend an hour on the wrong fix.

Error Meaning Where to look
SignatureDoesNotMatch (403) The seal did not match Clock, key, Region, request
RequestTimeTooSkewed (403) Request time and server time are too far apart Clock only
InvalidAccessKeyId (403) The access key ID does not exist in AWS's records The ID half of the pair
AccessDenied (403) Signed correctly, not allowed IAM policy, bucket policy, ACLs, Block Public Access, KMS, Object Lock, VPC endpoint policy, Organizations policies
AccessDenied with Request has expired Presigned URL is past its expiration Generate a new URL
ExpiredToken (400) The provided token has expired Refresh temporary credentials
AuthorizationHeaderMalformed (400) The authorization header is not valid Custom signing code, Region in the scope
InvalidRequest: Please use AWS4-HMAC-SHA256 (400) Request used the wrong signature version Upgrade the SDK; use SigV4
MissingAuthenticationToken (403) The request was not signed The client sent no credentials at all

When you fix the signature and then see AccessDenied, celebrate: the signature is fine now. The S3 403 troubleshooting topics to work through next are ACL settings, S3 Block Public Access, encryption settings, Object Lock, VPC endpoint policies, AWS Organizations policies, CloudFront distribution access and access point settings.

The "signing method" half of the message: old SDKs and custom code

The tail of the message says Check your key and signing method. The key part we covered. The signing method part is for old or unusual software.

S3 supports Signature Version 4 in every Region. Regions created after January 30, 2014 support only SigV4, and S3 no longer supports Signature Version 2 signing at all, so move to SigV4 before looking at any other fix. A request signed the old way is rejected with InvalidRequest and the message Please use AWS4-HMAC-SHA256. If you are running a very old SDK, upgrading it is the fix.

Two legacy switches are worth knowing. In older .NET code, setting AWSConfigsS3.UseSignatureVersion4 = true forces SigV4, which matters for the us-east-1 Region when server-side encryption with AWS KMS is involved. In the older Java SDK, the client can be told to use SigV4 with withSignerOverride("AWSS3V4SignerType"). If your code has to keep a legacy SDK, those are the switches to look for. And if the CLI itself returns strange errors, update it first, since running the latest version is the standard first step.

If you are on a very custom client, it also helps to remember that signatures are strict about the credential scope. The scope must end in aws4_request, its date must match the date in x-amz-date, and its service must match the host. Errors like Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP or Credential should be scoped to correct service are the general SigV4 wording for those mismatches.

One last look-alike: the same error text appears on non-AWS S3-compatible storage, because they copy the wording. If the hostname in your URL does not end in amazonaws.com, this is not an AWS error at all, and none of the Region and clock advice here applies until you know which vendor's server said no.

What it costs: the failed request is free, the outage is not

In general, S3 bucket owners are billed for requests with successful 200 OK responses and for HTTP 4XX client error responses, and are not billed for 5XX server errors. That would suggest a wave of failed requests shows up on the bill. For this error, it does not. As of September 2026, SignatureDoesNotMatch and RequestTimeTooSkewed are both on the list of specific 403 error codes that are not billed. So are InvalidAccessKeyId and MissingAuthenticationToken.

Response Billed to the bucket owner?
200 OK Yes
Most 4XX client errors Yes
SignatureDoesNotMatch, RequestTimeTooSkewed (403) No, on the not-billed list
AccessDenied from outside your account or organization No
5XX server errors such as 503 Slow Down No

The real cost is the feature that stopped working. Say Jake's page normally receives five photo uploads an hour on a Saturday. Three hours of failures, from 9 a.m. to noon, is fifteen customers who did not send a photo, and some of them will just phone another shop. That is the number to keep in mind when deciding how much monitoring this deserves.

One more cost-adjacent note: retrying does not help. A client that retries a request signed with a bad clock or a wrong Region will fail the same way every time. A retry loop with no upper limit turns a clear error into a noisy log. Fail fast, alert, fix the cause.

Read the error body, then replay the request

The one-line message hides the useful part. The response body is XML, and it includes a StringToSign block, the exact text S3 signed on its side, plus a request ID and an extended request ID (SDK exceptions often show these as request id and host id). Compare the string against what your client meant to send. A different date, a different Region in the scope or a different header list in there points straight at the cause.

  1. Capture the full response, not the truncated message your framework prints. In curl, add -v; in the CLI, add --debug.
  2. Read the StringToSign and compare the timestamp with real UTC now. Compare the Region in the scope with the bucket's Region.
  3. Run the same operation with a known-good implementation, such as the CLI, using the same credentials. If the CLI works and your code does not, the difference is in your code's credentials, Region or request.
  4. Retry from a second network. If it works there, suspect a proxy or firewall on the first.
  5. Change one thing at a time. Two changes at once teach you nothing.
# Verbose CLI call: shows the request that was signed and sent
aws s3api get-bucket-location --bucket YOUR_BUCKET --debug

# Replay a GET presigned URL and see the raw response
curl -v "YOUR_PRESIGNED_URL"

Compare what you find with the four-cause table, and resist the urge to start with the key.

Region, account and network edge cases

It works in the console but not the CLI. The console and the CLI do not sign with the same thing. The console uses your sign-in session. The CLI uses whatever credentials it resolves from command-line options, environment variables and files, in that order. A stale environment variable can make the CLI use a different identity than the one you are signed in as. Run aws configure list.

It works in the CLI but not the SDK. Different credential source, different Region setting or a different addressing style. Print the Region the SDK client is actually using and compare it with get-bucket-location.

It works from home but not the office. Suspect a proxy that edits headers or the query string. Test without the proxy.

The bucket belongs to another account. The signature itself does not care whose bucket it is. A signature that matches but lacks permission returns AccessDenied. Get the signature right first, then the policy. If the other account's owner will not add the policy, you are not getting in, and no amount of clock fixing changes that.

An organization policy, a VPC endpoint policy or Block Public Access is in the way. Those all produce access-denied errors, not signature errors. If you see SignatureDoesNotMatch, none of them is the cause.

The credentials are from IAM Identity Center or an assumed role. They expire. Re-authenticate and retry before you assume a deeper problem.

Keep it from coming back: a probe and a clock alarm

If you are the person who scrolled this far, you probably run something that must not fail on a Saturday. Three small guards cost almost nothing.

A one-file probe. Generate a presigned GET URL for a small, known object and fetch it. It should print 200. Anything else means one of the four causes has crept in. Run it from a scheduled job on the machine that signs.

#!/usr/bin/env bash
BUCKET=YOUR_BUCKET
KEY=healthcheck.txt
REGION=YOUR_REGION

URL=$(aws s3 presign "s3://$BUCKET/$KEY" --expires-in 60 --region "$REGION")
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL")

if [ "$CODE" != "200" ]; then
  echo "S3 signing probe failed: HTTP $CODE"
  aws configure list
  date -u +%Y%m%dT%H%M%SZ
  exit 1
fi

Note that a missing object gives a 404 instead, so make sure the object exists. And aws configure list masks the secret, so its output is safe to paste into your alert.

A clock alarm. Chrony reports its offset. The AWS Cloud Operations blog has a walkthrough on publishing instance clock accuracy, including a ClockErrorBound value, as a CloudWatch custom metric so you can alarm before the drift becomes an outage.

Boring configuration. Pin the Region in the client configuration instead of relying on a default. Use a role for anything on AWS. Log the time each URL was generated and when its credentials expire. If a URL must last longer than a role session, generate it with different credentials on purpose.

When nothing works: what to gather before you open a support case

If the clock is right, the key is right, the Region matches and the request is untouched, you have a case worth escalating. The goal is a message that lets someone else reproduce it without asking you three follow-up questions.

  • The full error response, including the StringToSign block.
  • The request ID and the extended request ID (host id).
  • The UTC timestamp of the failure.
  • The bucket name and its Region from get-bucket-location.
  • The endpoint you used, and whether it is a Region-specific one or a custom one.
  • The SDK or CLI name and exact version, and how the signature or URL was created.
  • The output of aws configure list from the machine that fails.
  • The result of the same request from a second machine or network.
  • Your curl -v output with the secret parts removed.

Never send a secret access key, and cut the X-Amz-Signature value out of any presigned URL you attach. The access key ID is fine to share; the secret is not.

Be realistic about what a support case can do. It can help you read what S3 saw. It cannot fix a clock on a machine it does not control, and it cannot make another account add a bucket policy. If the machine is a locked-down corporate laptop whose time is set by a system you do not own, only the person who owns that system can fix it, and the best thing you can do is hand that person the timestamp comparison.

Frequently asked questions

What does SignatureDoesNotMatch mean in S3?

It means the signature S3 calculated from the request it received does not match the signature your client sent. The seal on the request is different from the one S3 makes itself, so S3 returns HTTP 403. It is not a permissions error.

How do I fix "The request signature we calculated does not match the signature you provided"?

Work through four checks in order: sync the signing machine's clock, confirm the secret access key that is really in use (aws configure list), match the Region to the bucket (get-bucket-location), and make sure the request was not changed after signing. Retry after each one.

Can a wrong system clock cause SignatureDoesNotMatch in S3?

Yes, especially with presigned URLs, where even small drifts can invalidate signatures. A big gap on a normal signed request usually shows up as RequestTimeTooSkewed instead. Either way, the fix is to sync the clock of the machine that signed.

How far off can my clock be before S3 rejects the request?

As of September 2026, the timestamp on an authenticated request must be within 15 minutes (900 seconds) of S3's time. Treat that as a hard outer limit, not a target, and keep your clock accurate to within a second or two.

Why does S3 say "Check your key and signing method"?

It is the closing line of the standard error message. It points at your secret key and your signing method, but the same message also covers a wrong clock, a Region mismatch and a request that changed after signing, so do not stop at the key.

Does SignatureDoesNotMatch mean my IAM permissions are wrong?

No. Missing permissions produce AccessDenied. This error means the signature did not match, so fix the clock, key, Region or request first. Once the signature passes, any remaining refusal is a permissions matter.

Why does the AWS CLI work but my SDK gives SignatureDoesNotMatch?

They are probably using different credentials, a different Region or a different addressing style. The CLI checks command-line options, then environment variables, then files. Print the credential source and Region your SDK client uses and compare them with aws configure list and get-bucket-location.

Why is my S3 presigned URL giving SignatureDoesNotMatch?

Usually the request that uses the URL differs from the one that was signed: the wrong method, a header that was not signed, a different Content-Type, a different bucket or key spelling, a mismatched Region or a drifted clock. Check those in that order.

Why does my presigned PUT fail with SignatureDoesNotMatch when I add a Content-Type header?

If the URL was generated without a ContentType, an extra Content-Type header was not part of the signature. If it was generated with one, you must send the same value, including case. Match the header to how the URL was made.

Can the wrong Region cause SignatureDoesNotMatch in S3?

Yes. The Region is part of the credential scope inside the signature. Find the bucket's Region with aws s3api get-bucket-location --bucket YOUR_BUCKET and sign for that Region. Some Region mistakes appear under other names, such as IllegalLocationConstraintException.

Why do I get SignatureDoesNotMatch after rotating my access keys?

The most likely reason is that something still signs with the old secret, often an environment variable that overrides the credentials file, or a half-updated pair of ID, secret and session token. Run aws configure list and clear stale AWS_ variables.

Why is SignatureDoesNotMatch still happening after I fixed my clock?

The clock was probably not the cause, or there is a second cause behind it. Move on to the key, then the Region, then the request. A clean clock rules out one suspect, not the other three.

Can a proxy or firewall cause SignatureDoesNotMatch?

Yes. A proxy that edits headers or the query string changes the request after it was signed. Test the same request without the proxy or from another network. If it passes there, ask the network owner to stop rewriting S3 traffic.

Is SignatureDoesNotMatch the same as RequestTimeTooSkewed?

No. RequestTimeTooSkewed is its own 403 error, and it means the request time and the server's time are too far apart. SignatureDoesNotMatch is broader and covers key, Region and request changes as well as clock trouble.

Do failed S3 requests with SignatureDoesNotMatch cost money?

As of September 2026, no. Both SignatureDoesNotMatch and RequestTimeTooSkewed are on the list of 403 errors that S3 does not bill, even though most 4XX errors are billed. The outage still costs you whatever the broken feature was worth.

Why do I get SignatureDoesNotMatch on temporary credentials or an assumed role?

Temporary credentials need the session token as well as the key and secret, and the three parts must all belong to the same session. Expired sessions and stale environment variables cause the same symptom. Refresh the full set and retry.

If this saved you from a third round of key rotation, good. If it did not, the four causes are a checklist and not a guarantee, and your case may be a fifth one; tell me what you found and I will fix this page. Either way, Jake's wall clock can stay 20 minutes fast. The laptop cannot.

📌 If you keep one line from this page

SignatureDoesNotMatch is not a permissions error: the request that arrived was not the one you signed, so check the clock, the key, the Region and the request, in that order.

Fifteen minutes is the outer limit for the clock, not the target.

Revision note. Written September 30, 2026, for anyone whose keys look right and whose requests still bounce. Most days it turns out to be a clock or a Region, not you.

Related