Skip to main content

Overview

Tornado can deliver completed downloads straight into a Google Cloud Storage bucket you own. Authentication is a service account key: you create the service account in your own project, grant it a role on the bucket, download its JSON key, and paste that key into the dashboard. The dashboard asks for exactly three values: the bucket, the project ID, and the whole service account key file as one value. This page covers the dashboard path (the onboarding wizard and Settings › Default storage destination), which writes an organization-level configuration through POST /settings/storage. Credentials are stored in Azure Key Vault and are returned masked when the configuration is read back.
The dashboard route is not the same contract as the per-API-key endpoint POST /user/gcs. That endpoint names the credentials field service_account_json and additionally accepts folder_prefix and base_folder. The dashboard route names the field credentials_json and accepts three fields only. Do not copy a payload from one to the other.

Current limitations

Read these before you create anything in Google Cloud. Both are properties of the GCS route as it exists today, not Google restrictions.
“Test connection” does not contact Google. For GCS there is no probe object, no API call, and no credential check of any kind. The test loads the submitted configuration, confirms that bucket, project_id and credentials_json are present and non-empty, and returns:
The wizard renders that as a green banner reading “Credentials accepted and saved, verified on your first delivery.” While the request is in flight the button reads “Writing test object…”, which is the shared wizard label: for GCS nothing is written anywhere.A green result here proves that you filled in three boxes. It does not parse the JSON, and it says nothing about the key, the role, the project or the bucket. Every one of those failures surfaces later, on a real job. Verify with a real delivery before you consider the setup done.The S3 branch of the same endpoint was recently extended to write, read back, open and abort a multipart upload, and delete. None of that applies here. If you have read the AWS S3 or Cloudflare R2 page, discard what it says about the probe: for GCS no packet leaves Tornado.
The bucket must already exist. Tornado never creates it, and because the connection test makes no call, a bucket that does not exist is accepted at configuration time and fails on the first job.

Before you start

You need a Google Cloud project and, within it, permission to create a bucket, grant IAM on that bucket, and create a service account key.
Service account key creation may be blocked by organization policy. The constraints/iam.disableServiceAccountKeyCreation constraint prevents key creation in a project, and Google enforces it by default on projects created after 3 May 2024. If key creation fails with a policy error, an organization administrator must grant an exception for the project before you can continue. Tornado authenticates with a service account key only: Workload Identity Federation, attached service accounts and impersonation are not supported on this route.

1. Create the bucket

1

Open the bucket creation flow

In the Google Cloud console, go to Cloud Storage › Buckets, then select Create. Console layouts change; if the entry is not where you expect it, search for “Cloud Storage” from the top search bar.
2

Name it

Bucket names are globally unique across all of Google Cloud. Tornado additionally validates the name before every upload: 3 to 63 characters, lowercase letters, digits, hyphens and periods only, starting and ending with a letter or digit, no consecutive periods, and not shaped like an IP address. A name Google accepts but Tornado rejects will fail at job time, not at save time.
3

Choose the location

Pick a location type and location. See Choosing a location below.
4

Leave access control as uniform

Tornado authorises entirely through IAM and never reads or writes object ACLs, so uniform bucket-level access is the right setting and is what the rest of this page assumes. Note that uniform bucket-level access cannot be turned off again once it has been active on a bucket for 90 consecutive days.
Or from the command line:

Choosing a location

Google offers regions, dual-regions and multi-regions. For delivery buckets a single region is almost always correct:
  • A region has the lowest storage price and no replication charges. Dual-region and multi-region buckets add a replication charge on every write, and Tornado writes large media objects, so the difference is not marginal.
  • Redundancy across regions buys you nothing here. Delivered files are reproducible: if a bucket becomes unavailable you re-run the job.
For which region, colocate with the traffic that matters most to you. Each file is written once but may be read many times, so the region closest to whoever consumes the files is usually the right choice. If upload throughput matters more than read latency, the latency_ms reported by the connection test is a fair proxy for how far your bucket is from our workers.

2. Create the service account

1

Open Service Accounts

Go to IAM & Admin › Service Accounts in the console, select your project, then select Create service account.
2

Name it for this purpose only

Something like tornado-delivery. Use one service account per delivery destination so you can revoke it without affecting anything else.
3

Skip the project-level role

The creation flow offers an optional step titled along the lines of “Grant this service account access to project”. Skip it. Granting a role there applies it to every bucket in the project, present and future. You will grant the role on one bucket in the next step.
4

Copy the service account email

It looks like tornado-delivery@my-gcp-project.iam.gserviceaccount.com. You need it for the IAM grant.

3. Grant the role on the bucket

What Tornado actually does in your bucket

The role has to cover every operation the delivery path performs, not just the upload. The upload strategy is chosen by file size, and the large-file path is a composite upload, which is a different set of permissions from a plain write. Two of these deserve spelling out. Composite uploads read what they wrote. Files of 100 MB or more are uploaded as parts and then merged server-side with objects.compose, in batches of up to 32 sources. Google’s reference for that method lists storage.objects.create and storage.objects.get as always required, and storage.objects.delete as required “if the new composite object overwrites an existing object”. Tornado then deletes the temporary parts, which are named after the destination key with a .tornado.tmp.<uuid>.<nnnnn> suffix.
Two bucket settings interact badly with those temporary parts. On a bucket with object versioning enabled, each temp part leaves a noncurrent version behind when it is deleted, billed until a lifecycle rule reaps it. On a bucket with a retention policy or bucket lock, the temp-part deletes fail outright, and no role fixes that: the bucket configuration is the blocker, not the credential.
Signed URLs are signed locally, but read remotely. Tornado builds V4 signed GET URLs offline using the private key in your JSON file, so signing itself never calls Google and never fails on permissions. The permission check happens when the recipient opens the link, and the request is authorised as the signing service account. Without storage.objects.get, every signed link returns 403 even though the file is sitting in the bucket.

Minimum

Three permissions, and no more:
Put them in a custom role and grant it on the bucket, never on the project. That is the tightest configuration that works. Explicitly not needed: storage.objects.list (nothing enumerates the bucket), storage.objects.update, storage.buckets.get, storage.buckets.list, and all of storage.multipartUploads.* (that is the S3-compatible XML API, which this route does not use). The same three permissions, reached through a predefined role rather than a custom one: roles/storage.objectAdmin also works and is the answer you will find in most older write-ups; it is a slightly wider version of the same idea. roles/storage.objectViewer and roles/storage.objectCreator together are still not enough, because neither carries delete. Grant whichever you pick on the bucket, never on the project.
roles/storage.objectCreator is not enough, despite the hint shown under the GCS tile in the onboarding wizard (“The service account only needs roles/storage.objectCreator”). That role carries storage.objects.create and nothing else: no get, no delete.With objectCreator alone, small files written to object names that do not yet exist will appear to work, and then:
  • Every file of 100 MB or more fails, because objects.compose requires storage.objects.get.
  • Every signed download URL returns 403, because the reader’s GET requires storage.objects.get.
  • Re-delivering to an object name that already exists fails, because overwriting requires storage.objects.delete.
  • Temporary composite parts and the verify_credentials.txt probe object are left behind in the bucket, because the cleanup deletes are rejected and cleanup failures are not surfaced.
The wizard’s hint is wrong. Grant roles/storage.objectUser on the bucket, or a custom role with the three permissions above.

Granting it

1

Open the bucket's Permissions tab

In Cloud Storage › Buckets, select the bucket name, then select the Permissions tab.
2

Grant access

Select Grant access, paste the service account email into New principals, and choose Storage Object User from Select a role, or the custom role you created.
3

Save

Select Save. The grant applies to this bucket only.
Check the grant landed on the bucket and not the project: gcloud storage buckets get-iam-policy gs://tornado-downloads should list your service account, and gcloud projects get-iam-policy my-gcp-project should not.

4. Create and download the JSON key

1

Open the Keys tab

In IAM & Admin › Service Accounts, select the service account email, then select the Keys tab.
2

Create a JSON key

Select Add key › Create new key, choose JSON, and select Create. The file downloads immediately.
3

Store it somewhere sane

The private key cannot be downloaded again. Treat the file as a credential: keep it out of version control, and delete your local copy once it is in the dashboard.
The file is a JSON object beginning {"type": "service_account", ...}, roughly 2.3 KB. Tornado requires type to be exactly service_account and requires project_id, private_key and client_email to be present.

5. Enter the three fields

Open the dashboard, choose Google Cloud Storage as the destination, and fill in the fields. Labels differ between the onboarding wizard and the settings page; both write the same three API fields.

Pasting the key

Open the downloaded file in a text editor, select all, and paste. The field is a textarea and takes the raw file content, newlines and all. Do not reformat it.
Do not base64-encode the key for this field, and do not escape it by hand. Tornado parses the stored value directly as JSON. A base64 blob fails with “Invalid JSON in service account credentials”, and so does a value where the private_key newlines have been mangled by a copy that went through an intermediate tool. If you are constructing the API payload programmatically instead of pasting, let your JSON serialiser escape the string; do not do it yourself.
The equivalent payload:
Setting the storage destination is an organization-level action restricted to Admin and Owner members, and one provider is active at a time. Saving a GCS configuration replaces whatever provider was configured before.

Field constraints

A normal service account key is well under 3 KB, so the 10 000 character limit only bites if you paste something that is not a single key file.

Verify it for real

Because the connection test never contacts Google, the first honest signal is a delivered file. Run one small job and look in the bucket.
1

Submit a short download

2

Wait for the job to finish, then list the bucket

Delivered files land under the base folder, which defaults to videos/.
3

Then test a large file

A file under 5 MB exercises only storage.objects.create. Run a second job that produces at least 100 MB (a longer video at a higher resolution) to exercise the composite upload path, which is where an under-privileged role actually breaks.
4

Then test a read

Fetch the delivered object with the service account’s own credentials, which is the same authorisation a signed download URL uses:
A 403 here means the role is missing storage.objects.get, and every signed link you hand out will fail the same way.
Judge the outcome by the bucket, not by the job response. The s3_url field returned by GET /jobs/{id} and included in completion webhook payloads is produced by Tornado’s S3 signing path. For a GCS destination it does not currently resolve to your GCS object, so neither its presence nor a failure to fetch it tells you anything about whether the delivery worked. Look in the bucket.
If nothing arrives at all, check which configuration the job used before you suspect IAM. The job pipeline resolves storage per API key, from the configuration set through POST /user/gcs with that key’s x-api-key, falling back to the organization default and then to Tornado’s managed storage. A job that ran against managed storage still reports Completed, with the file in Tornado’s bucket rather than yours. See Custom Storage.

Troubleshooting

The dashboard’s connection test cannot produce any of these errors, because it makes no request. They surface on the first job instead. The worker treats 401, 403, 404, invalid_grant and Invalid JWT as permanent and fails the job immediately without retrying, reporting: “Invalid GCS storage credentials. Please check your service_account_json, project_id, and bucket name.” That single message covers several distinct causes.

Rotating the key

A service account can hold more than one key at a time, so overlap them and you rotate without an outage.
1

Create a second key

Add a new JSON key on the same service account. Both keys are now valid.
2

Update Tornado and confirm

Paste the new key into the dashboard and run a test job through to the bucket.
3

Disable, then delete, the old key

Disable the old key first and watch for failures before deleting it. Deletion is permanent.
Rotation also invalidates links you have already handed out. Google’s documentation is explicit that a signed URL grants access “until the expiration time for the URL is reached or the key used to sign the URL is rotated”, so retiring a key cuts short every outstanding link signed with it, ahead of its 24 hour expiry. Rotate when those links are expendable, or expect recipients to need fresh ones.

Not available on this route