> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tornadoapi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Azure Blob Storage

> Configure an Azure Storage account and blob container as your Tornado delivery destination from the dashboard

## Overview

Tornado can deliver completed downloads straight into a blob container in your own Azure Storage account. The dashboard asks for exactly three values: the storage account name, the container name, and a storage account access key.

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.

<Note>
  The dashboard route is not the same contract as the per-API-key endpoint
  [`POST /user/blob`](/features/custom-storage#azure-blob-storage). That endpoint
  names the container field `container` and additionally accepts `sas_token`,
  `folder_prefix` and `base_folder`. The dashboard route names the field
  `container_name` and accepts **three fields only**. Do not copy a payload from
  one to the other.
</Note>

## Current limitations

Read these before you create anything in Azure. Both are properties of the dashboard route as it exists today, not Azure restrictions.

<Warning>
  **The blob endpoint is not configurable.** The service URL is built as
  `https://<account_name>.blob.core.windows.net` and there is no field to
  override it. Storage accounts that live on a different endpoint suffix cannot
  be reached through this route: Azure Government
  (`blob.core.usgovcloudapi.net`), Azure operated by 21Vianet (China), and any
  storage account you front with a custom domain. Only the global public cloud
  suffix works.

  **SAS tokens are not accepted.** The dashboard authenticates with Shared Key
  only, using the storage account access key. The `sas_token` field that exists
  on `POST /user/blob` has no equivalent here, so do not generate a SAS for this
  configuration.
</Warning>

<Warning>
  **The container must already exist.** Tornado never creates it. The connection
  test writes a probe blob directly into the container you name, so a container
  that does not exist fails the test with `ContainerNotFound: azure:blob`.
</Warning>

## 1. Create a storage account

Skip to step 2 if you already have a dedicated account for Tornado deliveries. Read [Protect your access keys](#3-copy-an-access-key) first if you plan to reuse an existing one.

<Steps>
  <Step title="Open the storage account creation flow">
    In the [Azure portal](https://portal.azure.com), select **Storage accounts**
    from the portal menu, then select **Create**. Portal layouts change; if the
    menu item is not visible, search for "Storage accounts" from the top search
    bar.
  </Step>

  <Step title="Fill in the Basics tab">
    Pick a subscription and resource group, then set **Storage account name**.

    The name must be between 3 and 24 characters and can contain only lowercase
    letters and numbers. It is globally unique because it becomes a DNS label:
    `<name>.blob.core.windows.net`.

    **Standard** performance on a general-purpose v2 account is the right
    default for download delivery. Choose a region close to wherever you consume
    the files.
  </Step>

  <Step title="Leave Shared Key authorization enabled">
    Tornado authenticates with the account key. If your organization sets
    **Allow storage account key access** to **Disabled** (the
    `allowSharedKeyAccess` property, under **Settings › Configuration** on the
    account), Tornado cannot write to that account at all. See
    [Troubleshooting](#troubleshooting).
  </Step>
</Steps>

## 2. Create a container

<Steps>
  <Step title="Open Containers">
    In your storage account, find the **Data storage** section and select
    **Containers**.
  </Step>

  <Step title="Add the container">
    Select **+ Container**, give it a name, and select **Create**.

    Container names must be 3 to 63 characters, must start with a letter or a
    number, and may contain only lowercase letters, numbers and the dash (`-`)
    character.
  </Step>

  <Step title="Keep anonymous access off">
    Set the anonymous access level to **Private (no anonymous access)**. Tornado
    does not need public read: download URLs in webhook payloads are signed
    per-object, and signing works on a private container.
  </Step>
</Steps>

## 3. Copy an access key

<Steps>
  <Step title="Open Access keys">
    In your storage account, find the **Security + networking** section and
    select **Access keys**.
  </Step>

  <Step title="Reveal and copy">
    Select **Show keys**. Under **key1**, copy the **Key** value.

    Copy the **Key**, not the **Connection string**. The dashboard expects the
    bare base64 key. A connection string pasted into the key field fails at
    signing time before any request reaches Azure.
  </Step>
</Steps>

Viewing keys requires the `Microsoft.Storage/storageAccounts/listkeys/action` permission, which is carried by the **Owner**, **Contributor** and **Storage Account Key Operator Service Role** built-in roles.

### An account key is not a container credential

<Warning>
  A storage account access key grants full access to **every** resource in the
  storage account: all containers, all blobs, and the account's queues, tables
  and file shares. It cannot be scoped down to one container, and it also
  carries the power to mint SAS tokens for anything in the account. There are
  two keys (`key1` and `key2`) and both have identical, unrestricted scope.
</Warning>

Because the credential cannot be narrowed, narrow the account instead. **Create a storage account used only for Tornado deliveries.** Concretely, that buys you:

* **Blast radius.** The key you hand over reaches nothing but delivered downloads. On a shared account, the same string reaches your application data.
* **Rotation on your own schedule.** You can regenerate the key whenever you want without coordinating with other workloads that hold the same key.
* **Policies that fit one job.** Lifecycle rules, retention, soft delete, firewall rules and diagnostic logging can be tuned for delivery traffic without side effects elsewhere.
* **Clean attribution.** Storage metrics, transaction costs and account-level throughput limits reflect Tornado alone.

Tornado stores the key in Azure Key Vault, never in plaintext in its database, and masks it on read-back. That protects the key at rest; it does not change what the key can do if it leaks elsewhere.

### Minimum and recommended access

Azure is the odd one out here. Every other provider lets you write down a permission set; this route accepts a Shared Key, and a Shared Key has no permission set to write down. So the minimum and the recommendation are both about the *account*, and the RBAC and SAS vocabularies below are yardsticks for judging what you are handing over rather than things you can configure.

#### Minimum

| What                               | Value                                                                                                                                                                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Credential                         | A **storage account access key** (`key1` or `key2`), with **Allow storage account key access** left enabled on the account. Nothing else is accepted on this route                                                                         |
| Container                          | It must already exist. Tornado never creates one                                                                                                                                                                                           |
| Blob data operations actually used | `blobs/write` (Put Blob, Put Block, Put Block List, and the probe write), `blobs/delete` (the probe cleanup), `blobs/read` (whoever opens a download URL)                                                                                  |
| RBAC yardstick                     | **Storage Blob Data Contributor** scoped to the single container. Azure defines it with exactly the three blob data actions above: `.../blobs/read`, `.../blobs/write`, `.../blobs/delete`. No container management, no account management |

<Note>
  The RBAC row is a measurement, not an instruction. Assigning **Storage Blob Data Contributor** to a principal changes nothing for Tornado, because there is no Microsoft Entra ID, managed identity or user-delegation-SAS path in this route. It is here so you can see how far the account key over-grants: Contributor is three data actions on one container, the account key is everything in the account.
</Note>

#### Recommended

The minimum plus two things, neither of which narrows the key, because the key cannot be narrowed:

| Added                                                                 | Why                                                                                                                          |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| A **storage account dedicated to Tornado deliveries**                 | The credential is account-wide, so the account boundary is the only blast radius you control                                 |
| Both `key1` and `key2` kept valid, with Tornado holding one at a time | Rotation becomes a swap rather than an outage, and the swap is the only way to revoke a leaked key without breaking delivery |

Nothing else is worth adding. Container-level anonymous access, blob public access and account-level firewall exceptions are all things to leave alone or off.

#### If you use the per-API-key route instead

[`POST /user/blob`](/features/custom-storage#azure-blob-storage) does accept a `sas_token`. Sized correctly, that is an **account SAS** with signed services `ss=b` (blob), signed resource types `srt=o` (object), and the permission string **`sp=rwd`**. A **service SAS** scoped to the container (`sr=c`) with the same `rwd` works equally well; nothing Tornado does needs account-level or container-level operations.

Why those three letters and no others: Azure's permission table defines `w` as "Create or write content, properties, metadata, or block list", which is exactly what Put Blob, Put Block and Put Block List need, and `c` alone is not enough because a retry overwrites an existing blob. `d` covers the probe cleanup. `r` covers reads. `l` (list), `a` (add), `t` (tags), `i` and `p` are not used by anything, and `l` is not even valid on a blob-scoped SAS.

<Warning>
  A SAS gets you uploads and **no download URLs at all**. Minting the 24-hour download SAS requires the account key, and the job API's own Azure presign path is not implemented. A SAS-only Azure configuration delivers files that nobody can be handed a link to.
</Warning>

## 4. Enter the three fields

Open the dashboard, choose **Azure Blob** as the destination, and fill in the fields. Labels differ slightly between the onboarding wizard and the settings page; both write the same three API fields.

| Onboarding label | Settings label | API field        | What to paste                                                                         |
| ---------------- | -------------- | ---------------- | ------------------------------------------------------------------------------------- |
| Storage account  | Account Name   | `account_name`   | The bare account name, for example `tornadodeliveries`. Not a URL, not an FQDN.       |
| Container        | Container Name | `container_name` | The container name only, for example `downloads`. No leading slash, no path segments. |
| Account key      | Account Key    | `account_key`    | The base64 **Key** value from `key1` or `key2`.                                       |

The equivalent payload:

```json theme={null}
{
  "provider": "azure",
  "azure": {
    "account_name": "tornadodeliveries",
    "container_name": "downloads",
    "account_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=="
  }
}
```

<Note>
  Setting the storage destination is an organization-level action restricted to
  **Admin** and **Owner** members, and one provider is active at a time. Saving
  an Azure configuration replaces whatever provider was configured before.
</Note>

## 5. Test connection

The onboarding wizard's **Test connection** button runs a real round trip against your container before saving anything. On success the button reports the measured latency, for example "test object written and deleted in 340 ms".

What the probe actually does:

1. Opens a client against `https://<account_name>.blob.core.windows.net` with your account name and key as a Shared Key credential.
2. Uploads a small blob to `tornado-connection-test/<random>.txt` inside `container_name`, with overwrite enabled.
3. Deletes that blob.

Connect timeout is 5 seconds, read timeout is 10 seconds, and the whole probe is bounded at 20 seconds.

<Note>
  **The Azure probe is not the S3 probe.** The S3 branch of the same endpoint was extended to read the probe object back and to open and abort a multipart upload, so it now proves the read half. The Azure branch was not: it is still write then delete. Do not carry over what the [AWS S3](/storage/aws-s3) or [Cloudflare R2](/storage/cloudflare-r2) pages say about the read leg.
</Note>

<Accordion title="What a green result proves">
  * `<account_name>.blob.core.windows.net` resolves and is reachable from Tornado's network.
  * Shared Key authorization is permitted on the account and your key is accepted.
  * The container exists and is spelled correctly.
  * The credential can both create and delete a blob in that container.
  * The measured latency is a real network round trip, not an estimate.
</Accordion>

<Accordion title="What it does not prove">
  * **Nothing about reads.** The probe issues no GET.

    On Azure this is a smaller gap than the same sentence means on S3, and it is worth being precise about why. There is no read-only failure mode here: the credential is the account key, so anything it can write it can also read, and the download URL is a service SAS minted from that same key. What the probe cannot see is a restriction that bites on the read path specifically, such as an immutability policy, an encryption-scope rule, or a network rule that your customers hit and Tornado does not.
  * **Nothing about durability of the result.** It is a point-in-time check, not a monitor. Regenerating the key, tightening the account firewall, or disabling Shared Key access after the test all break delivery silently on the next job.
  * **Nothing about policies that trigger later.** Immutability policies, legal holds, encryption-scope restrictions, capacity limits or lifecycle rules that do not apply to the `tornado-connection-test/` prefix will not surface here.
  * **Nothing about the block upload path.** Blobs above 256 MB are uploaded as blocks and committed with Put Block List. The probe writes a few bytes in a single Put Blob, so it never exercises that path. Both are covered by the same write permission, so this is a throughput question rather than a permissions one.
  * **Nothing about the signed download URLs** included in `job.completed` webhook payloads. Those are signed locally, without contacting Azure.
  * **It does not run on every save.** Only the onboarding wizard probes. Saving from **Settings › Default storage destination** writes the configuration without testing it. To re-verify later, call `POST /settings/storage/test` with the same body you would send to `POST /settings/storage`.
</Accordion>

## Troubleshooting

Failures from the Azure branch of the connection test are formatted as `<AzureErrorCode>: azure:blob`, where the code is the error code Azure Storage returned. Match on the part before the colon.

| Error                                                  | Cause                                                                                                                                                                                                                                                       | Fix                                                                                                                                                                   |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ContainerNotFound: azure:blob`                        | The container does not exist in that storage account.                                                                                                                                                                                                       | Create it, or correct the name. Container names are lowercase; `Downloads` and `downloads` are not the same container.                                                |
| `AuthenticationFailed: azure:blob`                     | The key is wrong, was regenerated, or was copied with truncation or stray whitespace.                                                                                                                                                                       | Re-copy the **Key** value under `key1` or `key2`, with **Show keys** enabled.                                                                                         |
| `KeyBasedAuthenticationNotPermitted: azure:blob`       | The account has **Allow storage account key access** set to **Disabled**, so Shared Key requests are rejected outright.                                                                                                                                     | Tornado supports Shared Key only. Re-enable key access on this account under **Settings › Configuration**, or point Tornado at a different account.                   |
| `AuthorizationFailure: azure:blob`                     | Network restrictions rejected the request: public network access disabled, IP or virtual network rules, a service endpoint policy, or a network security perimeter.                                                                                         | The account must accept requests from Tornado's egress over the public endpoint. An account reachable only through a private endpoint cannot be used from this route. |
| `AccountIsDisabled: azure:blob`                        | The storage account or its subscription is disabled.                                                                                                                                                                                                        | Check subscription status and billing in the portal.                                                                                                                  |
| `InsufficientAccountPermissions: azure:blob`           | Write operations are not allowed against the account.                                                                                                                                                                                                       | Review account-level restrictions, then retry.                                                                                                                        |
| `InvalidResourceName: azure:blob`                      | The container name contains characters Azure does not allow.                                                                                                                                                                                                | Rename to lowercase letters, numbers and dashes only, 3 to 63 characters.                                                                                             |
| `HttpResponseError: azure:blob`                        | An Azure client error with no error code attached. In practice this is a local signing failure, almost always an `account_key` that is not valid base64: a truncated key, a key with embedded whitespace, or a connection string pasted into the key field. | Paste the **Key** value again, on its own, with nothing before or after it.                                                                                           |
| `timeout: no response from storage endpoint after 20s` | Nothing answered within the probe budget.                                                                                                                                                                                                                   | Usually a black-holed endpoint or DNS that resolves to an unreachable address, for example a private-endpoint-only DNS zone.                                          |
| A raw exception message with no `: azure:blob` suffix  | A transport-level failure below HTTP: DNS, TLS or a reset connection.                                                                                                                                                                                       | Most often a misspelled `account_name`, so `<account_name>.blob.core.windows.net` does not resolve. Check the account name character by character.                    |
| `internal error during storage test`                   | An unexpected exception inside the probe.                                                                                                                                                                                                                   | Retry once, then contact support with the timestamp.                                                                                                                  |

<Tip>
  A failed probe can leave a stray object behind. The probe writes then deletes
  `tornado-connection-test/<random>.txt`; if it fails between the two steps, the
  object stays. Objects under the `tornado-connection-test/` prefix are safe to
  delete.
</Tip>

## Rotating the access key

The two keys exist so that rotation does not cause an outage. Update Tornado before you regenerate.

<Steps>
  <Step title="Switch Tornado to the other key">
    If Tornado holds `key1`, save `key2` in the dashboard and confirm delivery
    on a test job.
  </Step>

  <Step title="Regenerate the key you retired">
    Back in **Access keys**, select **Regenerate** next to `key1`.
  </Step>
</Steps>

<Warning>
  Regenerating the key that Tornado currently holds breaks delivery immediately,
  and it also invalidates every signed download URL already issued. Download
  URLs in `job.completed` webhook payloads are service SAS URLs signed with the
  account key and valid for 24 hours; rotating the key that signed them makes
  outstanding links return 403 before they expire.
</Warning>

## Not available on this route

| Feature                                                        | Status                                                                                            |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| SAS token instead of the account key                           | Not accepted. Available on [`POST /user/blob`](/features/custom-storage#azure-blob-storage) only. |
| `folder_prefix` / `base_folder`                                | Not accepted. Available on [`POST /user/blob`](/features/custom-storage#folder-prefix) only.      |
| Custom endpoint, Azure Government, Azure China, custom domains | Not supported. The endpoint is fixed at `https://<account_name>.blob.core.windows.net`.           |
| Microsoft Entra ID, managed identity, user delegation SAS      | Not supported. Shared Key only.                                                                   |
| Automatic container creation                                   | Not supported. Create the container first.                                                        |
