Skip to main content

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
The configuration is always the same shape: five values, of which only the endpoint distinguishes one provider from another.
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.
Wasabi us-east-1 is the one region that also answers on a bare https://s3.wasabisys.com. Prefer the explicit https://s3.us-east-1.wasabisys.com form so the endpoint and the region field agree.
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:
  1. Leading and trailing whitespace is stripped.
  2. An accidental doubled scheme is collapsed, so https://https://host becomes https://host.
  3. If no scheme is present, https:// is prepended.
  4. Trailing slashes are trimmed.
Two consequences worth knowing:
  • Plain HTTP is preserved but never assumed. A MinIO instance on http:// must be entered with the scheme written out. minio.internal:9000 is rewritten to https://minio.internal:9000 and 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-bucket is 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.
The same configuration is editable later under Settings, where the S3 provider exposes Bucket, Region, Access Key ID, Secret Access Key and Endpoint (optional). The endpoint is optional only because the same form serves AWS S3, where it can be omitted. For every provider on this page it is required.
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.
The managed providers on this page (Wasabi, Backblaze B2, DigitalOcean Spaces, Scaleway, OVHcloud) serve both styles, so the default works. MinIO is the exception. MinIO only accepts virtual-host requests when the deployment sets 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

path_style is not exposed anywhere in the dashboard. Neither the onboarding wizard’s “Any S3-compatible” tile nor the Settings storage form renders a control for it. The wizard offers exactly five inputs: Endpoint URL, Bucket name, Region, Access key, Secret key. The field exists in the API model and is honoured by the backend, but a dashboard-only user cannot set it.
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 include path_style. This is the same route the dashboard uses, so the saved configuration is identical in every other respect.
Dry-run it first against the same payload shape, which validates without persisting:
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:AbortMultipartUpload and s3:DeleteObject on 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 NotImplemented at step 3 will fail every delivery above 50 MB.
It does not prove:
  • 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 to videos/* 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_url points 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.
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

Scope the resource to the whole bucket, not to the delivery prefix. Delivered files land under videos/, but the connection test writes to tornado-connection-test/ at the bucket root. Narrow the policy to tornado-downloads/videos/* and the test fails with AccessDenied: s3:PutObject even though real deliveries would have worked.

Troubleshooting

The error 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 reports internal 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 no s3: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. The presigned URL is virtual-host style and your MinIO does not have MINIO_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 reporting Completed. See Custom Storage for the per-key model.

”missing required field(s): …”

Returned by the offline structural fallback, which only runs when the S3 SDK itself is unavailable on the server. If you see it against production, the response is telling you the round-trip never happened. Retry, and if it persists it is a Tornado-side problem rather than a credentials problem.