After upgrading to the v0.16.x branch, the background maintenance task “Purge blob store” fails consistently with an S3 SignatureDoesNotMatch (403) error when using Cloudflare R2 as the blob backend.
The underlying problem is that Stalwart’s S3 dispatcher implementation is including an empty “content-length:” and a “content-type:text/plain” header inside the CanonicalRequest payload during a HTTP DELETE operation. Because a standard DELETE request has no body payload, Cloudflare R2 calculates the signature according to strict AWS specs without these malformed entries, leading to an authentication signature mismatch.
All other operations (such as initial writes and verification reads) function flawlessly; the failure is isolated strictly to the blob deletion/purge code path introduced in the rewrite.
Expected Behavior
Stalwart should successfully purge orphaned objects from the S3/R2 bucket during maintenance. The internal S3 client should construct a standard, bodyless HTTP DELETE request and sign it without injecting empty content-length or default text content-type headers into the CanonicalRequest signature string.
Actual Behavior
The maintenance task fails with a 403 Forbidden error originating from crates/store/src/dispatch/blob.rs:221. The R2 storage engine returns a SignatureDoesNotMatch XML response because Stalwart’s calculated signature does not match the signature calculated by Cloudflare.
Reproduction Steps
Deploy Stalwart Mail Server v0.16.7.
Configure an S3 Blob Storage backend pointing to a Cloudflare R2 bucket.
Ensure the API keys have full administrative read/write/delete privileges and IP whitelisting allows connections.
Trigger or wait for the automatic “Purge blob store” maintenance task to execute.
Check the logs or the administration panel to see the failed task state.
The deployment environment is a standard Linux VPS.
Credentials and bucket access permissions have been verified independently using standalone S3 clients with IP whitelisting active; manual object operations (including deletions) are fully functional outside of Stalwart.
The signature calculation logic mismatch became apparent immediately following the platform migration to the v0.16 release stream.
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
Let me look at the relevant source code to understand the issue.
The Stalwart S3 backend calls self.bucket.delete_object(...) from the rust-s3 library. The signature canonicalization issue (empty content-length: and content-type:text/plain headers being included in the DELETE request’s CanonicalRequest) is happening inside the rust-s3 crate, not directly in Stalwart’s own code.
This is a confirmed bug in the S3 DELETE path introduced in the v0.16.x rewrite. The CanonicalRequest your log shows clearly includes:
content-length:
content-type:text/plain
…for what should be a bodyless DELETE request. This causes Cloudflare R2 (which strictly follows AWS SigV4 specs) to reject the request with SignatureDoesNotMatch, while AWS itself is apparently more lenient.
Root cause: The rust-s3 library (the dependency Stalwart uses) is incorrectly injecting content-length (empty) and content-type: text/plain headers into the canonical request for DELETE operations. Since DELETE has no body, these headers should not be present in the signed headers list at all. Stalwart’s own delete_blob code at crates/store/src/backend/s3/mod.rs just calls bucket.delete_object(...) – the defect is in how rust-s3 constructs and signs that request.
Workaround options until a fix is released:
There is no configuration-level workaround for this within Stalwart, since the signing logic is in the rust-s3 dependency.
You can temporarily disable the “Purge blob store” maintenance task in the admin panel to stop the errors, with the understanding that orphaned blobs will accumulate until a fix is applied.
Track the Stalwart server repository for a dependency bump or patch to rust-s3 that corrects the DELETE signing behavior.
This is a genuine bug that will need to be fixed at the code level (either by patching the rust-s3 dependency or by manually stripping those headers before signing in delete_blob). The maintainers will need to address it in a future release.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.
I can confirm this exact same behavior on v0.16.x using Cloudflare R2. My logs show the exact same CanonicalRequest hash mismatch due to the content-type:text/plain header on the empty DELETE.
I can confirm this also applies for all sort of s3 signatures when using ceph rgw as s3 store.
Since upgrading Ceph RGW to Ceph 20.2.2 Tentacle, Stalwart 0.16.11 fails against a Ceph-backed S3-compatible blob store with SignatureDoesNotMatch generally.
The same Ceph RGW endpoint, bucket, credentials, and path-style configuration work correctly with other S3 clients, for example the PHP AWS SDK. This appears to be a SigV4 canonical request / signed-header compatibility issue between Stalwart’s S3 blob backend, rust-s3, and stricter Ceph RGW request validation/canonicalization in Ceph 20.2.2.
A working PHP AWS SDK request signs only the stable SigV4 headers:
SignedHeaders=host;x-amz-content-sha256;x-amz-date
The failing Stalwart / rust-s3 request appears to include additional HTTP/client headers in SignedHeaders, for example:
SignedHeaders=accept;host;x-amz-content-sha256;x-amz-date
or similar volatile transport/client headers.
This matters because Ceph RGW verifies S3 requests using AWS SigV4-compatible authentication. In SigV4, every header listed in SignedHeaders must match exactly when the server reconstructs the canonical request.
Headers such as the following are not required for S3 SigV4 authentication and may be injected, normalized, removed, or changed by the HTTP stack or proxies:
accept, accept-encoding, user-agent, connection, amz-sdk-request, amz-sdk-invocation-id
If one of these volatile headers is included in SignedHeaders, Ceph RGW can calculate a different canonical request and reject the request with SignatureDoesNotMatch.
I completely agree with the sentiment about rust-s3. If a fundamental operation like a bodyless DELETE request is being signed incorrectly and goes unfixed for months, it is hard to trust it as mature enough for critical mail infrastructure.
Because of this, I decided to completely drop Cloudflare R2 and migrate my entire blob storage to native Amazon S3. My philosophy going forward is that “S3 is S3” really means it belongs to Amazon. While third-party implementations like R2 or Backblaze are great for raw storage, AWS is the reference standard. Sticking to native AWS S3 means I don’t have to worry about strict SigV4 parsing failures from third-party endpoints rejecting malformed headers that AWS itself handles gracefully.
To fix this once and for all:
I used rclone to sync my entire blob bucket directly from Cloudflare R2 to an AWS us-west-1 S3 bucket.
I put Stalwart into recovery mode, updated the BlobStore singleton via the stalwart-cli to point to AWS S3, and injected my new IAM credentials.
I restarted the server.
Overall, I had to sacrifice about 5 to 10 minutes of downtime for the mail server, but the result is 100% stability. The “Purge blob store” maintenance task now runs flawlessly without a single 403 SignatureDoesNotMatch error. Considering how incredibly cheap native AWS S3 is for standard mail volumes, getting rid of this headache and achieving full compatibility was absolutely worth the switch.