What this covers
Tornado talks to any storage that speaks the S3 API. Beyond AWS S3 and Cloudflare R2, which have their own tiles in the dashboard, the generic S3 path is what you use for:MinIO
Self-hosted, on your own hostname and port
Wasabi
Hot cloud storage
Backblaze B2
Via the B2 S3-compatible API
DigitalOcean Spaces
Per-datacenter endpoints
Scaleway
Object Storage
OVHcloud
S3 Object Storage
Requests are always signed with AWS Signature Version 4. This is forced, not negotiated.
A provider or gateway that only supports SigV2 will not work.
Endpoint and region per provider
Every endpoint below was checked against the provider’s own current documentation. Use the provider’s console to read your actual region, since several providers assign it per bucket rather than per account.OVHcloud:
s3.{region}.io.cloud.ovh.net is the current endpoint, and OVHcloud calls it
“the preferred endpoint to access OVHcloud Object Storage”. The older
s3.{region}.perf.cloud.ovh.net is “maintained for backward compatibility purposes only, to
allow tools that don’t support AWS’s recent Express_One_Zone storage class to continue
operating”. Tornado is not one of those tools. Use the io form.DigitalOcean: some AWS SDKs insist on
us-east-1 when creating a bucket. Tornado never
creates buckets, it only writes objects into one you already own, so put your datacenter code
in the region field.What Tornado does to your endpoint string
Before the S3 client is built, the endpoint is normalised:- Leading and trailing whitespace is stripped.
- An accidental doubled scheme is collapsed, so
https://https://hostbecomeshttps://host. - If no scheme is present,
https://is prepended. - Trailing slashes are trimmed.
- Plain HTTP is preserved but never assumed. A MinIO instance on
http://must be entered with the scheme written out.minio.internal:9000is rewritten tohttps://minio.internal:9000and the TLS handshake will fail. - The bucket must not appear in the endpoint. Only the scheme, host and port are trimmed.
A path such as
https://s3.fr-par.scw.cloud/my-bucketis kept verbatim and the bucket ends up in the request twice.
Filling the fields in the dashboard
The onboarding wizard has a dedicated tile for this. In Destination, pick Any S3-compatible (the wide tile at the bottom of the grid, below Google Drive), then fill the Credentials step:1
Endpoint URL
The provider host from the table above, with scheme. The placeholder shows the expected
shape:
https://minio.internal.example.dev:9000.2
Bucket name
The bucket, on its own. It must already exist; Tornado does not create it.
3
Region
The region value from the table above. The field cannot be left empty.
4
Access key
Maps to
access_key_id.5
Secret key
Maps to
secret_access_key. Write-only in the UI, and read back masked afterwards.6
Test connection
Runs a live probe against the bucket before anything is saved. Read
what it proves before you trust the green tick.
Credentials are written to Azure Key Vault. The organisation document keeps only the provider
name and a vault reference. If Key Vault is unreachable the save is refused with a 503 rather
than falling back to storing the secret in the database.
Addressing style: path-style vs virtual-host
An S3 client can address a bucket in two ways:- Virtual-host style:
https://my-bucket.s3.example.com/key. The bucket becomes a subdomain, so the provider needs a wildcard DNS record and a matching TLS certificate. - Path-style:
https://s3.example.com/my-bucket/key. The bucket is the first path segment.
MINIO_DOMAIN, and its own documentation is explicit that “omitting this setting directs MinIO
AIStor to only accept the default path-style requests”
(core settings).
A stock MinIO therefore needs path-style, and enabling virtual-host on it means adding a
wildcard DNS entry and a certificate covering *.your-minio-host as well.
The current limitation
What actually breaks without it is narrower than you might expect, because two different components sign URLs:
So on a stock MinIO configured through the UI alone, the file uploads correctly, but
Test connection fails and the
download_url in your job.completed webhooks points at a
hostname that does not resolve.
Workaround: set it through the API
Configure the organisation’s storage directly and includepath_style. This is the same route
the dashboard uses, so the saved configuration is identical in every other respect.
Both routes require an Admin or Owner session token, not an
x-api-key. Re-saving from the
Settings form afterwards will drop path_style, because the form does not send a field it
does not render. Re-run the curl if you edit storage in the UI.What “Test connection” proves
The probe is a real network call against your real bucket, not a credential format check. It runs five steps and reports the round-trip latency:
No part is ever uploaded, so the whole probe moves 23 bytes regardless of how many steps it runs.
Each step names itself in the error string, so a failure tells you which permission is missing
rather than just
AccessDenied.
It proves:
- The endpoint resolves, the TLS handshake succeeds and the host speaks S3.
- The bucket exists and is reachable at that endpoint with the region you supplied.
- The credentials carry
s3:PutObject,s3:GetObject,s3:AbortMultipartUploadands3:DeleteObjecton that bucket. - The read works as the identity your download links are signed with, which is the reason the read leg exists at all.
- The addressing style currently in effect works for the endpoint.
- The provider actually implements the multipart API. Not every S3-compatible gateway does, and a
gateway that returns
NotImplementedat step 3 will fail every delivery above 50 MB.
- That a real delivery will fit. The probe moves 23 bytes and uploads no parts.
- That your policy allows the delivery prefix. The probe writes to
tornado-connection-test/at the bucket root, so a policy scoped tovideos/*passes the test and fails the job. - That a recipient can fetch the link. Tornado reads from its own network; a bucket policy conditioned on source IP, or a private MinIO your customers cannot reach, behaves differently for them.
- That the presigned link is addressed correctly. On a stock MinIO the probe can pass while the
download_urlpoints at a hostname that does not resolve. See Addressing style.
Permissions
Minimum
Exactly the four actions the probe exercises and delivery uses, and nothing else:Minimum
Do not add
s3:CreateMultipartUpload, s3:UploadPart or
s3:CompleteMultipartUpload. They are not IAM actions. AWS’s multipart permission table maps
all three to s3:PutObject, and every S3-compatible provider that accepts AWS-style policy
documents follows the same mapping. AbortMultipartUpload is the only step of the lifecycle
with its own action name, which is exactly why it is the one people leave out.Recommended
The minimum plus one bucket-level action:Recommended
s3:ListBucket is not needed at all. Tornado never enumerates the bucket: it writes to a key it
computed itself, signs a URL for that key, and deletes by key.
Providers with their own dialect
Troubleshooting
Theerror string returned by the connection test is deliberately compact. For S3 failures it is
<ProviderErrorCode>: s3:<Operation>, where the operation is whichever of the probe’s five steps
failed first: PutObject, GetObject, CreateMultipartUpload, AbortMultipartUpload or
DeleteObject.
”internal error during storage test” means your endpoint is malformed
This is the least informative error and the most common one to hit while setting up a new provider. It says nothing about URLs, but that is usually exactly what it is. The S3 client is constructed before the probe’s own error handling begins. If the SDK rejects the endpoint string outright, for example because it contains a space, an illegal character, or an over-long hostname label left behind by a bad paste, the rejection happens during construction and is caught by the generic handler, which reportsinternal error during storage test with no
detail.
When you see it, check the endpoint before anything else:
1
No spaces, no stray characters
A trailing space inside the value survives the trim if it is in the middle. Retype the host
rather than pasting it.
2
Scheme written once
https://https://... is collapsed automatically, but a mangled paste such as
https://host/.provider.com/ is not repaired.3
No bucket, no path
Host and optional port only.
4
Not the console URL
Provider consoles live on different hostnames to the S3 API. Wasabi’s console is
console.wasabisys.com, which is not a storage endpoint.Test passes, downloads return 403
The probe reads, so a policy with nos3:GetObject at all now fails the test rather than passing
it. A green test with 403 links means the read is allowed on tornado-connection-test/ but not
on the delivery path: look for a policy Resource narrowed to something other than bucket/*.
Failing that, the link was signed with a key that has since been rotated, which kills a presigned
URL regardless of its expiry.
Test passes on MinIO but webhook links do not resolve
The presigned URL is virtual-host style and your MinIO does not haveMINIO_DOMAIN set. Either
set path_style: true through the API, or configure MINIO_DOMAIN with the matching wildcard
DNS and certificate.
Job reports Completed but the bucket is empty
This is not an S3-compatibility problem. Storage configured per API key does not apply to other keys, and a job sent with an unconfigured key silently lands in Tornado’s managed storage while still reportingCompleted. See Custom Storage for the per-key
model.
