Home › DevOps › S3 SignatureDoesNotMatch — Fix SigV4 Signing Fast
Advanced 5 min · September 23, 2026

S3 SignatureDoesNotMatch — Fix SigV4 Signing Fast

SignatureDoesNotMatch means your secret, clock, region, or encoding differs from AWS.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 19 min
  • ✓Basic AWS IAM and S3 concepts
  • ✓Comfortable with AWS CLI commands
  • ✓Understanding of HTTP requests
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • SignatureDoesNotMatch means the signature you sent doesn't match the one AWS computed — same request, different inputs on each side
  • The four usual suspects: wrong/rotated secret key, clock skew over 15 minutes, bad URL encoding of special chars, region/endpoint mismatch
  • Confirm identity and clock first: aws sts get-caller-identity plus NTP offset check take 30 seconds
  • For presigned URLs, compare the canonical request byte-for-byte — one encoded slash breaks the whole signature
✦ Definition~90s read
What is S3 SignatureDoesNotMatch Fix?

SignatureDoesNotMatch is S3's way of saying: I recomputed the request signature using the secret key I have for your access key, and it doesn't equal the signature you sent. The mechanism is SigV4 (Signature Version 4): your SDK builds a canonical request (method, URI, sorted query params, signed headers, payload hash), hashes it into a StringToSign that includes the date and region scope, then HMAC-signs it with a key derived from your secret, the date, the region, and the service.

★
Imagine you and a bank clerk each solving the same puzzle with your account number, date, and a secret code — matching answers mean trust.

AWS repeats the identical computation server-side. Any single input differing — one wrong secret byte, a date 16 minutes off, a %2F versus /, a us-west-2 scope against a us-east-1 bucket — produces a totally different signature, and S3 rejects the request without telling you which input diverged.

Note what S3 does not do: it never reveals which input was wrong (that would help attackers forge signatures), and the two strings in the error look alike because they're both valid HMAC outputs — of different inputs. Debugging is therefore reconstructive: you re-derive each input independently (are my keys current? is my clock synced? is my region right? is my URL encoded exactly once?) until the computation agrees.

What this error is NOT: it's not AccessDenied (your identity is recognized; only the math failed), not NoSuchBucket (routing worked), not a permissions problem (fixing IAM policies changes nothing), and not an expired presigned URL (that's a distinct ExpiredToken/AccessDenied-with-expiry message). SignatureDoesNotMatch specifically indicts the signing inputs — key, clock, encoding, region — so IAM policy edits and bucket-policy relaxations are always wasted motion against it.

Plain-English First

Imagine you and a bank clerk each solving the same puzzle with your account number, date, and a secret code — matching answers mean trust. Now your watch runs 20 minutes fast, or you miscopy the secret. Your answer won't match, and the bank rejects you though you're the real holder. That's SignatureDoesNotMatch: you and AWS ran the same signing math with different inputs — key, clock, encoding, or region. Find the differing input and the signatures agree again.

Your S3 uploads worked yesterday. Today every PutObject fails with SignatureDoesNotMatch, and the error message helpfully shows two long base64 strings that look identical but aren't. No code shipped, no keys changed (that anyone admits to), and the bucket policy is untouched. The signing math is deterministic — same inputs, same signature — so something in your inputs drifted.

This error loves credential rotations, fresh EC2 instances with unsynced clocks, and filenames with plus signs, tildes, or Unicode characters. It also haunts presigned URLs generated in one region and used against another, and SDK calls where a proxy quietly rewrites the query string after signing. Each cause produces the identical error message, which is why teams burn hours rechecking the key that's actually fine.

The trap is regenerating keys as the first move. New keys fix a rotated-secret cause but change nothing for clock skew, encoding, or region bugs — and now you've invalidated every other service sharing those credentials. Diagnose before you rotate.

By the end of this article you'll run the 30-second identity-plus-clock check, decode what AWS expected via the StringToSign, fix each of the four root causes, and build signing code that survives rotations, special characters, and multi-region deployments.

How SigV4 Turns Four Inputs Into One Signature

SigV4 is a chain of HMACs, and every link names its inputs explicitly. First your SDK builds the canonical request: HTTP method, canonical URI, canonical (sorted, encoded) query string, canonical headers, signed-header list, and the payload hash. That blob is hashed, then embedded in the StringToSign alongside the algorithm name, the request timestamp, and the credential scope (date, region, service, terminator). Finally the signing key — derived by HMAC-chaining your secret with date, region, and service — signs that string. Change any input bit and the output is unrecognizably different; that's cryptographic design, not flakiness.

This determinism is your debugging superpower: identical inputs always reproduce identical signatures. When AWS rejects you, one of your inputs differs from what AWS used — and AWS's inputs are ground truth (its stored secret for your key, its clock, the region the bucket lives in, the bytes it actually received). Your job is re-deriving each of your four inputs independently until you find the liar.

The error message's two strings are the two HMAC outputs — yours and AWS's — and comparing them character-by-character is theater. They differ everywhere because HMAC avalanches; position of first difference carries zero information. Ignore the strings, interrogate the inputs.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Reproduce the signing inputs independently (no SDK involved)
# 1. What AWS thinks your identity is (proves key validity in 5s)
aws sts get-caller-identity --query Arn --output text

# 2. What your clock says vs reality (SigV4 tolerance: 15 min)
date -u
chronyc tracking 2>/dev/null | grep -E 'System time|Leap status|Stratum'
# Want: System time offset < 1s. Suspect: anything over ~60s.

# 3. What region the bucket actually lives in (signature scope must match)
aws s3api get-bucket-location --bucket my-bucket --query LocationConstraint
# null = us-east-1. Your signer must use exactly this region.

# 4. What bytes S3 received (enable SDK debug to capture canonical request)
# boto3.set_stream_logger('botocore', logging.DEBUG)  -> look for
# 'CanonicalRequest' and 'StringToSign' lines, diff vs a working host
⚠ Don't Compare the Two Signatures Character by Character
HMAC outputs avalanche — one input bit flips half the output characters. The strings in the error will always look 'almost the same but different everywhere.' Interrogate the four inputs (key, clock, region, encoding) instead.
📊 Production Insight
Log the StringToSign (never the secret) on signing failures in staging. When production fails, diffing staging's known-good StringToSign against production's isolates the divergent input without touching credentials.
🎯 Key Takeaway
SigV4 chains method, URI, params, headers, date, region, and secret into one HMAC. Any differing input breaks it — so verify each input, not the output strings.

Wrong or Rotated Secrets: Verify Before You Rotate

Secret problems come in three flavors: the key was rotated (old secret deleted, app still holds it), the key was copied wrong (trailing newline, truncated env var, shell escaping mangled a slash or plus), or the app loads a different credential than you think (stale profile, expired SSO token, instance-profile race during rotation). All three produce identical SignatureDoesNotMatch, and only the first is fixed by rotation — the other two survive new keys untouched.

Verify before rotating with the cheapest possible probe: aws sts get-caller-identity using the exact credentials the app loads (same env vars, same profile, same container). Success proves the pair is live and correctly copied, eliminating secrets from suspicion in seconds. Failure with InvalidClientTokenId names the access key as the problem; failure with SignatureDoesNotMatch on STS itself implicates the secret bytes or the clock — check both.

Audit the credential path, not just the value. Dump aws configure list to see which profile, env, or role actually supplies credentials, check for a trailing %0A from a copy-paste newline, and confirm SSO tokens haven't expired (sso sessions silently go stale and some SDKs fall back to older cached keys). When rotation is genuinely needed, generate the new pair first, deploy everywhere, verify, then delete the old — overlap, never gap.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Prove the EXACT credentials the app uses (same env, same container)
# Dump the resolution chain first — which source actually wins?
aws configure list
# Shows: access_key from [env|shared-credentials-file|iam-role], region source

# Test with the app's literal environment (copy from the pod/task def)
env -i AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
  aws sts get-caller-identity --region us-east-1
# Returns ARN -> keys live and correctly copied. Suspect clock/region/encoding.
# InvalidClientTokenId -> access key wrong/deleted. SignatureDoesNotMatch here -> secret bytes or clock.

# Catch the classic copy-paste injuries
printf '%s' "$AWS_SECRET_ACCESS_KEY" | wc -c   # must be 40 chars, no trailing newline
printf '%s' "$AWS_SECRET_ACCESS_KEY" | grep -q $'\n' && echo 'TRAILING NEWLINE FOUND'

# Rotation done right: create second pair, deploy, verify, THEN delete old
aws iam create-access-key --user-name uploader --query 'AccessKey.[AccessKeyId,SecretAccessKey]'
📊 Production Insight
Instance-profile races during rotation serve half-old credentials for seconds — enough to 413 a burst of uploads. Consumers should retry signing failures once after a short sleep before alerting, absorbing rotation transients.
🎯 Key Takeaway
Test credentials with sts get-caller-identity before rotating. Check length, newlines, source precedence, and SSO expiry — rotate with overlap, never with a gap.

Clock Skew Past 15 Minutes: The Silent Signature Killer

SigV4 bakes the request timestamp into the StringToSign, and AWS rejects any request whose timestamp differs from its own clock by more than 15 minutes. This is replay-attack protection, and it makes every unsynced host in your fleet a signature-failure machine — while HTTP, DNS, database, and all other traffic work perfectly. The selectivity is what makes clock skew so misleading: 'everything works except S3' reads like a credentials problem, not a time problem.

Fresh instances, new container hosts, and laptops off the corporate VPN are the classic victims. The incident pattern is unmistakable in hindsight: old hosts fine, new hosts failing, identical code and keys. AMI baking commonly masks chrony, Docker hosts without time-sync drift on suspend/resume, and some minimal images ship no NTP client at all — inheriting whatever the hypervisor clock says.

Build time-sync into the platform, not the runbook. Images should assert sub-second offset at boot and refuse traffic until synced; orchestrators should expose clock offset as a health signal; and the signing-failure runbook must list the clock check before key rotation, with rotation gated behind it. A one-line date comparison during the incident would have saved the 18,000 failed uploads in this article's story.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# The 30-second clock verdict (run on the FAILING host, not your laptop)
date -u                                              # what the host believes
curl -s http://169.254.169.254/latest/meta-data/ 2>/dev/null | head -1  # EC2 sanity
chronyc tracking 2>/dev/null | grep -E 'System time|Leap|Stratum'
ntpdate -q pool.ntp.org 2>/dev/null | grep -i offset
# Verdict: offset > 60s -> sync NOW and retry before touching keys

# Emergency resync, then immediate retry of the failing call
sudo chronyc -a makestep        # chrony: step the clock immediately
# or: sudo ntpdate -s pool.ntp.org  (ntpdate hosts)
aws s3api head-bucket --bucket my-bucket && echo 'SIGNING AGAIN'

# Permanent: boot-time gate in user-data / entrypoint (fail loud, never skewed)
# offset=$(chronyc tracking | awk '/System time/ {print $4}');
# awk "BEGIN{exit !($offset > 1.0)}" && { echo 'CLOCK UNSYNCED'; exit 1; }
📊 Production Insight
Expose clock offset as a metric (node_time_offset_seconds exists in most exporters) and alert at 60s — a full order of magnitude before SigV4's 15-minute cliff. Skew should page as infrastructure, not surface as signing errors.
🎯 Key Takeaway
AWS tolerates 15 minutes of skew; past that every signature fails while everything else works. Sync time first, rotate keys never-until-proven.

Special Characters and Double Encoding in Keys and URLs

S3 object keys with spaces, plus signs, tildes, or non-ASCII characters are the encoding minefield of SigV4. The canonical URI must encode each path segment exactly once with a precise safe set — and the classic failures are double-encoding (%2B becoming %252B when two layers each encode), plus-as-space confusion (form-encoding + versus path-encoding %20), and SDK-versus-proxy disagreements where a proxy normalizes the URL after your code signed the original. Each produces a signature over bytes AWS never received.

Presigned URLs concentrate every encoding risk into one string that passes through browsers, email clients, and chat apps — any of which may re-encode or wrap it. A presigned URL generated with one encoding and fetched with another fails exactly like a bad key. The diagnostic is byte comparison: log the canonical request your signer built, capture the actual request line S3 received (SDK debug logging shows both), and diff them. They must match byte-for-byte; the first differing byte names the layer that re-encoded.

Standardize encoding at the SDK boundary and never hand-roll it. Use your language's canonical quoter with the S3 safe set, generate presigned URLs server-side with the SDK (not string templates), and add integration tests with adversarial filenames — spaces, plus, tilde, Unicode, and a literal % — so the next encoding regression fails in CI instead of production.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Byte-compare what you signed vs what S3 received
python3 - <<'EOF'
from urllib.parse import quote
key = 'photos/vacation 2026/a+b~c%percent.txt'
# S3 canonical URI: encode each segment, safe set is / ~ (plus alphanumerics)
canonical = '/'.join(quote(seg, safe='~') for seg in key.split('/'))
print('sign this:', canonical)
# sign this: photos/vacation%202026/a%2Bb~c%25percent.txt
# If your wire log shows %252B or a literal '+', that layer re-encoded.
EOF

# Generate presigned URLs with the SDK — never string templates
aws s3 presign 's3://my-bucket/photos/vacation 2026/a+b~c%percent.txt' --expires-in 900

# Fetch verbosely and confirm the params arrive unmangled
curl -v "$(aws s3 presign 's3://my-bucket/photos/vacation 2026/a+b~c%percent.txt' --expires-in 900)" -o /dev/null 2>&1 | grep -E '^> GET|HTTP/'
# Adversarial CI filenames: 'a b', 'a+b', 'a~b', 'caf\u00e9', '100%.txt'
🔥Presigned URLs Are Encoding Fragility Concentrated
A presigned URL carries the signature in the string itself, so any re-encoding by browsers, proxies, or chat apps invalidates it. Generate with the SDK, transport carefully, and test with adversarial filenames in CI.
📊 Production Insight
Proxies that 'normalize' URLs (decoding %2F to / before forwarding) silently break SigV4 for keys containing slashes-as-data. If SDK-direct works but proxied fails, compare the request line on both sides of the proxy.
🎯 Key Takeaway
Encode each key segment exactly once with the S3 safe set. Diff the signer's canonical request against the wire bytes — first difference names the culprit layer.

Region and Endpoint Mismatches That Masquerade as Bad Keys

The credential scope embeds the region, so a request signed for us-west-2 and sent to a us-east-1 bucket fails with the same SignatureDoesNotMatch as a wrong secret. The usual setups: a client defaulting to us-east-1 against a bucket that lives elsewhere, a presigned URL minted in one region and redeemed in another, or an S3-compatible endpoint (MinIO, LocalStack, VPC endpoint) where the SDK signs for 's3' in one region while the endpoint expects another. Multi-region deployments hit this the moment traffic shifts.

Diagnose with ground truth, not config archaeology: ask AWS where the bucket lives (get-bucket-location; null means us-east-1), then check what region your signer used (SDK debug shows the credential scope line: date/region/s3/aws4_request). Mismatched strings are a complete diagnosis. Also verify the endpoint hostname matches the signing style — path-style versus virtual-hosted-style addressing changes the canonical URI, and mixing styles between signer and sender breaks signatures identically.

Fix by making region explicit everywhere: client constructors with the bucket's region, presigned-URL generation in the bucket's region, and per-bucket clients in multi-region apps rather than one global client. Environment-variable region leaks (AWS_REGION set for one service, inherited by another) are a recurring source — scope region config to the S3 client, not the process.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Ground truth: where does the bucket live vs what did you sign for?
aws s3api get-bucket-location --bucket my-bucket --query LocationConstraint
# null -> us-east-1. Anything else -> use EXACTLY that string.

echo "env regions: AWS_REGION=$AWS_REGION AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION"
grep -rn 'region' ~/.aws/config 2>/dev/null

# SDK debug exposes the scope AWS will check (must match bucket region)
# boto3.set_stream_logger('botocore', logging.DEBUG)
# Look for: 'CredentialScope: 20260923/us-west-2/s3/aws4_request'
# If scope says us-west-2 but bucket lives in us-east-1 -> mismatch found.

# Fix: explicit per-bucket client, explicit presign region
python3 - <<'EOF'
import boto3
s3 = boto3.client('s3', region_name='us-east-1')  # bucket's real region
url = s3.generate_presigned_url('put_object',
    Params={'Bucket': 'my-bucket', 'Key': 'uploads/a b.txt'}, ExpiresIn=900)
print(url)
EOF
📊 Production Insight
us-east-1's null LocationConstraint is a perennial trap — code that treats null as 'no region' signs with a default that may differ per SDK. Normalize null to us-east-1 explicitly in every region-resolution path.
🎯 Key Takeaway
The signature scope embeds the region; signer region must equal bucket region. Make it explicit per client and per presigned URL.

SDK Signing Pitfalls: Proxies, Versions, and Credential Chains

Modern SDKs sign correctly out of the box — failures come from what surrounds them. Egress proxies that rewrite query strings or normalize paths invalidate signatures computed pre-proxy; test by comparing a direct SDK call against an identical proxied one. SDK upgrades occasionally change default signing behavior (addressing style, payload-signing for streaming, checksum headers added to the signed set); pin versions and read changelogs when failures coincide with a bump. Custom middleware that adds headers after signing (or strips signed ones) breaks the signed-headers contract just as thoroughly.

Credential chains add a second failure class that wears the same error costume. SSO sessions expire, instance metadata serves briefly inconsistent keys during rotation, and shared config files get edited by automation that appends duplicate profiles. When failures are intermittent and host-correlated, suspect the chain: dump the resolved source (aws configure list), check SSO token freshness, and correlate failure windows with rotation timestamps.

Harden the signing path as infrastructure: wire-log canonical requests in staging, pin SDK versions with lockfiles, forbid proxies from mutating S3 URLs (CONNECT tunneling instead of rewriting), and retry signing failures once after refreshing credentials — absorbing rotation transients without paging anyone.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Intermittent failures? Suspect the chain and the path, not the key
aws configure list                      # which source actually supplies creds?
aws sso login --profile uploader 2>&1 | head -2   # refresh stale SSO tokens

# Direct vs proxied: identical call, two paths — divergence indicts the proxy
env -u https_proxy -u HTTPS_PROXY python3 s3probe.py   # direct: expect OK
python3 s3probe.py                                     # proxied: fails = proxy rewrites

# Capture what was actually signed (staging + incident hosts)
python3 - <<'EOF'
import logging, boto3
boto3.set_stream_logger('botocore', logging.DEBUG)
s3 = boto3.client('s3', region_name='us-east-1')
s3.head_bucket(Bucket='my-bucket')
# Diff 'CanonicalRequest:' and 'StringToSign:' blocks between working/failing hosts
EOF

# Pin signing behavior: lockfiles + changelog check on every SDK bump
# pip freeze | grep -i boto ; npm ls @aws-sdk/signature-v4
📊 Production Insight
Header-adding sidecars (tracing, auth) that mutate requests post-signing are invisible in app logs and fatal to signatures. Sign last or exclude sidecar headers from the signed set — verify with wire logs on both sides.
🎯 Key Takeaway
SDKs sign correctly; proxies, upgrades, and credential chains break it around them. Compare direct versus proxied, pin versions, and log canonical requests.
● Production incidentPOST-MORTEMseverity: high

The NTP Drift That Rejected 18,000 Uploads Overnight

Symptom
At 1:12 AM, S3 PutObject failures jumped from zero to 100% on 22 newly scaled EC2 instances — roughly 18,000 failed uploads by morning. Older instances in the same autoscaling group kept working fine, which made it look like a bad AMI or a poisoned deploy. The error was uniformly SignatureDoesNotMatch, and the on-call engineer rotated the IAM user keys at 2:40 AM, briefly fixing nothing while invalidating credentials for two other working services.
Assumption
The team assumed leaked or rotated credentials because 'signatures mean keys' — the dominant mental model. They rotated keys twice, compared base64 strings character by character, and then assumed a bucket-policy change had broken signing, spending 3 hours diffing CloudTrail-unrelated IAM history. Nobody checked the system clock for 6 hours because 'NTP just works' and the instances were fresh from a golden AMI that supposedly synced on boot.
Root cause
The new AMI's chrony service was masked (a leftover from image baking), so the 22 fresh instances free-ran from a hypervisor clock 17 minutes fast. SigV4 embeds the request date in the StringToSign and AWS rejects skew beyond 15 minutes — the fleet sat 2 minutes past the cliff. Older instances ran the previous AMI with working NTP and signed fine. Key rotation and policy edits were pure noise; the second rotation actually extended the outage to previously healthy consumers until the new keys propagated.
Fix
Three changes closed it permanently. First, chrony was unmasked and verified (chronyc tracking showing sub-second offset) in the AMI build, with a boot-time gate that blocks traffic until offset is under 1s. Second, user-data gained an NTP assertion: ntp check fails the instance health check, so skewed hosts never join the target group. Third, the runbook's signing-failure page now starts with the 30-second clock-plus-identity check (date -u versus NTP, aws sts get-caller-identity) before any key rotation is permitted — rotation requires a second engineer's approval.
Key lesson
  • Clock skew is a signing input, not background trivia. Anything past 15 minutes breaks every SigV4 call on the host while leaving all other traffic healthy — check the clock before touching keys.
  • Key rotation as a first move can widen the outage. Rotating shared credentials invalidates healthy consumers too; diagnose (identity + clock + region + encoding) before rotating anything.
  • Fresh instances are the prime suspects when old ones work. Identical code failing only on new hosts points at the environment (clock, AMI config, subnet endpoints) — diff the hosts, not the code.
Production debug guideFive ordered checks — identity, clock, region, encoding, SDK — that isolate the divergent input.5 entries
Symptom · 01
All S3 calls fail and you need to rule the key in or out in 30 seconds
→
Fix
Prove the key pair is valid and belongs to who you think: AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=yyy aws sts get-caller-identity --region us-east-1. A returned ARN means the keys are live and correct — stop suspecting rotation and move to clock/region/encoding. An InvalidClientTokenId means the access key is wrong or deleted; a SignatureDoesNotMatch here specifically (rather than on S3) still points at the secret or clock, so check both before rotating.
Symptom · 02
Keys are valid but S3 still rejects every signature
→
Fix
Measure clock offset — SigV4 dies past 15 minutes of skew: date -u; chronyc tracking | grep -E 'System time|Leap status'; ntpdate -q pool.ntp.org 2>/dev/null | grep offset. If offset exceeds ~60s, force a sync (sudo chronyc -a makestep or sudo ntpdate -s pool.ntp.org) and retry the S3 call immediately. Fresh instances, containers without a time sync sidecar, and laptops off VPN are the classic skewed hosts.
Symptom · 03
Only some buckets fail, or presigned URLs fail while SDK calls pass
→
Fix
Check region scope mismatch — the signature embeds the region: aws s3api get-bucket-location --bucket my-bucket --query LocationConstraint; echo $AWS_REGION $AWS_DEFAULT_REGION; grep -rn 'region' ~/.aws/config app/config/*.py 2>/dev/null. A us-east-1 bucket (LocationConstraint null) signed with a us-west-2 scope fails. Force the client region to the bucket's real region and regenerate presigned URLs there — a URL signed for one region never validates in another.
Symptom · 04
Only filenames with spaces, plus signs, or Unicode fail
→
Fix
Inspect raw encoding — S3 SigV4 needs each path segment encoded exactly once: python3 -c "from urllib.parse import quote; print(quote('photos/a+b c~d.txt', safe='/~'))" and compare against what your code signs versus what the HTTP layer sends. Double-encoding (%252B) and plus-as-space are the two classics. Log the canonical URI your signer produced and the actual request line on the wire — they must match byte-for-byte or the signature dies.
Symptom · 05
SDK calls fail after upgrades, proxies, or credential-chain changes
→
Fix
Isolate the signer from the environment: enable SDK wire logging (boto3.set_stream_logger('botocore', logging.DEBUG)) and diff the canonical request and StringToSign against a known-good host; verify no proxy rewrites query strings (curl -v the presigned URL and compare params); and dump the credential source with aws configure list to catch a stale profile, an expired SSO token, or an instance-profile race serving half-rotated keys. Pin botocore and re-test — signing behavior changes ship in minor versions.
SignatureDoesNotMatch Causes — How to Confirm and Fix Each
Root CauseHow to ConfirmFixPrevention
Wrong, truncated, or stale secret keysts get-caller-identity fails; key length != 40 or trailing newline presentFix the copy or rotate with overlap (new pair live before old deleted)Gate rotation behind passing identity check; approve rotations
Clock skew beyond 15 minuteschronyc/ntpdate shows large offset on failing hosts; old hosts fineResync time immediately and retry before touching keysBoot-time NTP gate; alert on offset metric past 60s
Region/scope mismatch (signer vs bucket)Credential scope region differs from get-bucket-location resultSign with the bucket's exact region; regenerate presigned URLs thereExplicit per-bucket clients; never rely on ambient region env vars
Double encoding or special-char manglingOnly special-char keys fail; canonical request differs from wire bytesEncode once with the S3 safe set; generate URLs with the SDKCI tests with adversarial filenames (spaces, +, ~, Unicode, %)
Proxy rewriting or SDK/credential-chain driftDirect works but proxied fails; failures track deploys or rotationsTunnel instead of rewriting; pin SDKs; refresh SSO/role credentialsWire-log canonical requests in staging; retry-once on signing failures
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
aws sts get-caller-identity --query Arn --output textHow SigV4 Turns Four Inputs Into One Signature
aws configure listWrong or Rotated Secrets
date -u # what the host believesClock Skew Past 15 Minutes
python3 - <<'EOF'Special Characters and Double Encoding in Keys and URLs
aws s3api get-bucket-location --bucket my-bucket --query LocationConstraintRegion and Endpoint Mismatches That Masquerade as Bad Keys
aws configure list # which source actually supplies creds?SDK Signing Pitfalls

Key takeaways

1
SignatureDoesNotMatch means divergent signing inputs, not broken S3 or IAM.
2
Check identity (sts) and clock (NTP) in 30 seconds before anything else.
3
Never rotate keys as a first move
diagnose, then rotate with overlap.
4
Signer region must equal bucket region; normalize us-east-1's null explicitly.
5
Encode keys exactly once; diff canonical request against wire bytes.
6
SDKs sign fine
proxies, upgrades, and credential chains break it around them.

Common mistakes to avoid

5 patterns
×

Rotating keys as the first response to every signing failure

Symptom
Rotation fixes nothing (cause was clock/region/encoding) while invalidating healthy services sharing the credentials.
Fix
Run identity-plus-clock checks first; require approval for rotation and always overlap new-before-delete-old.
×

Comparing the two signatures in the error character by character

Symptom
Hours spent staring at base64 strings that differ everywhere regardless of cause, yielding zero diagnostic information.
Fix
Ignore the outputs and interrogate the four inputs — key validity, clock offset, region scope, and wire encoding.
×

Editing IAM or bucket policies to fix a signing error

Symptom
Policy diffs and access grants change nothing because the identity was never the problem — the math was.
Fix
Recognize the error taxonomy: SignatureDoesNotMatch is signing inputs; AccessDenied is permissions. Never cross the streams.
×

Hand-building presigned URLs with string templates

Symptom
Intermittent failures on special-char filenames that the SDK-generated URLs never exhibit.
Fix
Generate all presigned URLs with the SDK's signer in the bucket's region, and transport them without re-encoding.
×

Assuming us-east-1 (null LocationConstraint) means 'no region needed'

Symptom
Multi-region apps sign some requests with ambient region env vars that don't match the bucket, failing sporadically.
Fix
Normalize null to us-east-1 explicitly and give every S3 client an explicit region matching its bucket.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Every S3 call fails with SignatureDoesNotMatch but nothing shipped. What...
Q02SENIOR
Old instances sign fine but fresh autoscaled hosts all fail. What do you...
Q03SENIOR
Only filenames with + and spaces fail. Walk me through the diagnosis.
Q04SENIOR
Why is comparing the two signatures in the error message useless?
Q05SENIOR
Design a signing path that survives rotations, regions, and proxies.
Q01 of 05JUNIOR

Every S3 call fails with SignatureDoesNotMatch but nothing shipped. What's your first command?

ANSWER
aws sts get-caller-identity with the app's exact credentials, plus date -u against NTP on the failing host. The first proves key validity in seconds; the second catches the 15-minute clock cliff. Together they eliminate or implicate the two most common causes before any rotation or policy edit.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I fix SignatureDoesNotMatch with IAM policy changes?
02
How much clock skew breaks S3 signatures?
03
Why do presigned URLs fail while SDK calls with the same key work?
04
Do I need to restart my app after fixing the clock?
05
Can a proxy cause SignatureDoesNotMatch even with correct keys?
06
How do I test signing with adversarial filenames?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
🔥

That's Cloud. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Nginx 504 Gateway Timeout Fix
10 / 13 · Cloud
Next
Lambda Task Timed Out Fix
→