Fix S3 SignatureDoesNotMatch and AWS4-HMAC-SHA256 Errors in Django
Fix SignatureDoesNotMatch and The authorization mechanism you have provided is not supported, please use AWS4-HMAC-SHA256 in django-storages: set the bucket region and signature version, fix presigned URLs, clock skew, and custom endpoints for R2, Spaces, and MinIO.
Problem statement
Uploads fail, or images load locally but not in production, with one of these:
botocore.exceptions.ClientError: An error occurred (SignatureDoesNotMatch) when calling
the PutObject operation: The request signature we calculated does not match the
signature you provided. Check your key and signing method.
<Error><Code>InvalidRequest</Code><Message>The authorization mechanism you have
provided is not supported. Please use AWS4-HMAC-SHA256.</Message></Error>
Every S3 request is signed with your secret key, the region, the time, and details of the request. S3 recomputes the signature and compares. A mismatch means one of those inputs differs between your app and S3: the wrong secret, the wrong region, an old signature version, a clock that is off, or a URL that was changed after signing.
- Set the bucket's real region:
AWS_S3_REGION_NAME = "eu-central-1". - Force Signature Version 4:
AWS_S3_SIGNATURE_VERSION = "s3v4". - Check the secret key for stray whitespace or a rotated value: test with
aws s3 ls s3://bucket --profile app. - Make sure the server clock is synced:
timedatectlshould showSystem clock synchronized: yes. - For R2, Spaces, or MinIO, set
AWS_S3_ENDPOINT_URLand the provider's region value.
Step-by-step solution
1. Set the region and signature version
Regions launched after 2014, such as eu-central-1, ap-south-1, and ca-central-1, accept only Signature Version 4, and v4 signatures include the region. A missing or wrong region produces both errors above, especially for presigned URLs:
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-media",
"region_name": "eu-central-1",
"signature_version": "s3v4",
},
},
}
Find the bucket's region with aws s3api get-bucket-location --bucket myapp-media. A null location means us-east-1.
2. Verify the credentials themselves
AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... aws s3 ls s3://myapp-media --region eu-central-1
If the CLI fails with the same error, the secret is wrong. Common causes: a trailing newline or space copied into an environment variable, a key rotated in IAM but not in the app, or quotes included literally in a .env file parsed by a tool that does not strip them.
3. Check the clock
Signatures include a timestamp, and S3 rejects requests more than 15 minutes off. Containers use the host clock:
timedatectl
sudo timedatectl set-ntp true
A skewed clock usually also shows RequestTimeTooSkewed, but some clients surface it as a signature mismatch.
4. Do not modify presigned URLs
A presigned URL's signature covers its host, path, and query string. Rewriting the host to a CDN, re-encoding the path, or adding query parameters breaks it. If you serve media through CloudFront, use custom_domain with public objects or CloudFront signed URLs, not S3 presigned URLs with the domain swapped:
"OPTIONS": {
"bucket_name": "myapp-media",
"region_name": "eu-central-1",
"querystring_auth": True, # presigned S3 URLs
# "custom_domain": "media.example.com", # only for public objects via CDN
}
Also, file names with spaces or unicode must be encoded once, by boto3. Pre-encoding names in your code leads to double encoding and a mismatch.
5. S3-compatible providers
Cloudflare R2, DigitalOcean Spaces, Backblaze B2, and MinIO use the S3 API with their own endpoints:
"OPTIONS": {
"bucket_name": "myapp-media",
"endpoint_url": "https://<account>.r2.cloudflarestorage.com",
"region_name": "auto", # R2; Spaces uses e.g. "fra1"
"signature_version": "s3v4",
"addressing_style": "virtual", # MinIO often needs "path"
}
Using the AWS default endpoint with R2 or Spaces keys gives InvalidAccessKeyId or a signature mismatch.
Explanation
Signature Version 4 derives a signing key from the secret, the date, the region, and the service, then signs a canonical form of the request. S3 does the same from its side. Any difference produces SignatureDoesNotMatch; S3 cannot tell you which input differed, only that the result did. The AWS4-HMAC-SHA256 message is more specific: the client used the older v2 scheme, which newer regions do not accept. Recent boto3 versions default to v4, but explicit old settings or an unknown region can still produce v2-style or wrongly scoped requests.
Edge cases / notes
- Bucket names with dots. Virtual-hosted addressing over HTTPS fails certificate checks for buckets like
media.example.com; use path addressing or rename the bucket. - Content-Type changes. Presigned uploads signed with a
Content-Typemust be sent with exactly that header from the browser. - IAM roles. On EC2, ECS, or EKS, prefer instance or task roles over static keys; they avoid copied-secret problems entirely.
Next steps
- If the signature is fine but access is refused, see Fix Django S3 Upload Access Denied.
- Review the storage setup with Store Django Media Files on S3.
- For browser uploads, read Upload Files Directly to S3 From Django.
FAQ
What does "please use AWS4-HMAC-SHA256" mean?
The request was signed with the old Signature Version 2. Set signature_version to s3v4 and the correct region.
Why do uploads work but presigned download links fail?
Links are often signed with a different region or then altered, for example by swapping the host for a CDN domain.
Can clock drift really cause this?
Yes. Signatures include the time, and S3 rejects requests far outside the allowed window.
Do I need region_name for S3-compatible providers?
Yes, use the value the provider documents, such as auto for R2.