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 may be enforced by your organization. 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. Choose according to your availability requirements, data-location constraints and the provider’s current storage, replication and transfer prices. Re-running a download is only an option while the original media remains available. Compare upload duration and recipient download latency using real deliveries to candidate locations. The GCS dashboard test returns latency_ms: 0 without contacting Google, so it cannot measure distance to Tornado’s 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 default file-upload strategy depends on size and concurrency. Large files can use parallel composite upload, while an optional worker mode uses XML multipart. Streaming uploads have their own strategy; a small dashboard probe does not exercise these paths. Two of these deserve spelling out. Composite uploads read what they wrote. When selected, the composite path uploads parts and merges them 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 does not call Google or validate object-read 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

For the default JSON/composite paths, allow these object operations:
A custom role granted on this bucket avoids giving the service account access to other buckets in the project. Inherited project permissions can also authorize access, but are broader. The default JSON/composite path does not enumerate the bucket. Its operation set does not require storage.objects.list, storage.objects.update, storage.buckets.get or storage.buckets.list.
The optional worker mode GCS_XML_MULTIPART=1 uses XML multipart with the same service-account credential. That mode additionally needs the multipart permissions for creating and aborting uploads. Confirm the deployed upload mode and its IAM requirements before adopting a three-permission custom role. The dashboard does not expose an XML toggle or accept HMAC keys.
A predefined role can be easier to maintain than a custom role; inspect its current permission list before granting it: 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. Prefer granting the selected role on the destination bucket rather than the whole project.
roles/storage.objectCreator is not enough. It does not provide the object-read and deletion permissions this delivery flow needs. The current onboarding hint recommends roles/storage.objectAdmin; a narrower role must still cover every operation used by your upload mode.With objectCreator alone, small files written to object names that do not yet exist will appear to work, and then:
  • A composite upload fails when objects.compose needs storage.objects.get. The default file path selects composite upload from 100 MiB when enabled and parallelism is available.
  • 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.
Grant a bucket-scoped role covering the operations of your deployed upload mode, and validate both a real delivery and a recipient download.

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 small file may use only the simple-upload path. Run a second job that produces at least 100 MiB (a longer video at a higher resolution) and verify the large-file mode used by your deployment. Successful simple upload does not prove composite or XML multipart permissions.
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 can indicate missing storage.objects.get or another access-policy restriction. Also open the actual delivery link from the recipient’s network; a successful command-line read alone does not prove recipient access.
Verify both the object and its delivered URL. The worker uses the actual GCS backend to create the signed URL, stores it with the completed job, and includes it as s3_url in the per-job completion webhook. Job polling can return that recorded URL while it is fresh. The legacy field name does not imply an S3 destination. A missing or expired link is not proof that upload failed: also check the object in your bucket.Organization-level custom webhooks sent by the dashboard are a separate path: that dispatcher currently presigns S3/Azure only, so its download_url is not available for GCS.
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. When Google’s response identifies the cause, the job’s error says exactly what to fix, and failure_kind classifies it (the job status is failed): Other permanent refusals keep the generic message “Invalid GCS storage credentials. Please check your service_account_json, project_id, and bucket name.”, which 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