Skip to content
downpipes docs

Choosing a destination: provider, region, addressing, storage class and object lock

A destination is the bucket your sealed archives land in, and nothing can run without it. The console never stores a destination it has not verified: the primary action on the form is Verify and save, and it reaches the bucket, authenticates, and performs a real write probe before anything is kept. A typo fails at the form, not during your first backup.

This page walks the decisions on that form in the order you meet them, with the recommended path and what the other options translate to. Where the four providers differ from each other, one row each, is destination providers compared.

R2, S3-compatible, Google Cloud or Azure Blob

Cloudflare R2 is the default because the endpoint is derived from your account id, so there is less to type and less to mistype. Ticking “this bucket is in a different Cloudflare account” reveals an account id field and derives that account’s endpoint instead, which is the off-site copy most estates should hold.

The default is a typing convenience, not a security recommendation. R2 cannot enforce Object Lock on any bucket, and a destination in the same Cloudflare account as your sources shares that account’s credential and blast radius: a compromise that reaches the account reaches the backup too. The preferred primary leg is an off-account, Object-Lock-capable destination (S3 with Object Lock, Google Cloud Storage with per-object retention, or Azure Blob with version-level immutability); R2 stays a valid destination and a good secondary leg. The console’s own Destinations screen carries a calm note to this effect whenever nothing you hold is off-account with immutability, described in the 3-2-1 distinctions.

S3-compatible makes every field explicit: the endpoint URL, the region, and optionally the addressing style and storage class. Use it for AWS S3 or any S3-compatible store you already trust.

Google Cloud is Google Cloud Storage, reached through its S3-interoperable API. Google publishes one interop endpoint and it is neither per-bucket nor per-region, so the console fills the endpoint in for you and makes it read-only: there is nothing to choose and nothing to mistype. You supply the bucket and an HMAC key pair, which in Google Cloud are created under Cloud Storage settings as interoperability keys. They are a different kind of credential from a service-account JSON key, and only the HMAC pair works here.

Two fields disappear when you choose Google Cloud, because Google Cloud Storage cannot honour them and the engine refuses each one by name rather than letting your first backup fail:

FieldWhy Google Cloud Storage cannot honour itSet it where instead
Storage classGoogle has its own class names and downpipes does not translate between the two vocabularies, so an Amazon class name is refused rather than sent and silently rejected.Set the class you want on the bucket itself in Google Cloud.
AssumeRoleSTS is an Amazon Web Services mechanism with no Google equivalent, so a role ARN would never be assumed.Authenticate with the HMAC key pair for the service account.

Object lock is offered for Google Cloud, and whether it works depends on the bucket rather than on the provider. Google Cloud Storage does implement S3 Object Lock through its interoperable API, but only for a bucket created with per-object retention. Create the bucket that way (gcloud storage buckets create ... --enable-per-object-retention) and the engine’s live probe reads Object Lock as enabled, arms your policy, and the store enforces it: a delete inside the retention window is refused. Point downpipes at a bucket created without it and the probe reads not-enabled. The save is refused rather than storing a destination that would drop every locked write. You cannot turn per-object retention on afterwards, so it is a decision to make when the bucket is created.

For a Google Cloud destination, leave the region at auto. The engine signs every S3-shaped request with the region you give it (engine/src/dest/s3.ts), and auto is the value to use with Google’s interoperability endpoint.

Azure Blob is the fourth button on this picker, and it changes enough of the form to have its own section. The section below is the whole of how you configure it.

Azure Blob Storage

Azure Blob Storage is written to by a client of its own rather than through the S3-compatible one, because Azure does not speak the S3 API. It has its own wire protocol, its own object model, its own listing document and its own authentication scheme, so the engine carries a dedicated Azure client and its own credential handling for it (AzureBlobDestination in engine/src/dest/azure-blob.ts, with signAzureSharedKey in engine/src/dest/azure-sharedkey.ts for the common case). Aiming the S3-compatible destination at a real Azure endpoint answers 403 AuthenticationFailed on its first call, and no arrangement of the S3 fields makes that work.

The provider picker offers R2, S3-compatible, Google Cloud and Azure Blob. Choosing Azure Blob relabels two boxes, because Azure does not authenticate with an S3-style key pair: the endpoint is your storage account’s own blob host, and the credential is the storage account NAME plus one of its access keys. The engine derives the store from the endpoint host and from nothing else (providerForEndpoint, engine/src/dest/provider.ts), so the button changes what the form asks you for rather than what the engine does with it.

The endpoint is https://<account>.blob.core.windows.net, where <account> is your storage account name. It must be https, and a non-https Azure endpoint is refused when the destination is built rather than on its first write, because the account key and the archive bytes would otherwise cross in clear. The container name goes in the bucket field. The region box is unused here: a Shared Key signature carries no region, so whatever you leave there has no effect on an Azure destination.

The credential is an account name and an account key

An Azure destination does not authenticate with an S3-style key pair. In the common case it authenticates with the storage account name and one of that account’s access keys. That is a different kind of credential wearing the same two boxes. The account name goes in the access key id field and the access key goes in the secret field, so the stored config shape and the envelope encryption that protects the secret at rest are the ones every other destination already uses (buildDestinationInner, engine/src/dest/factory.ts). Copy the key exactly as the Azure portal shows it: standard base64 that the signer decodes to bytes before signing. Two other credential kinds are accepted as well, and they are set out below.

The account name has to be the account in the endpoint host. The engine checks the two against each other before it builds anything (azureAccountFromHost, engine/src/dest/provider.ts). A Shared Key signature is scoped to one storage account, and Azure answers a mismatch with an error that names neither side of it, so a mismatch here would read as a permission problem for as long as you cared to look. Saving runs a live probe, and the probe builds the destination first, so a mismatched pair is refused at the form with a message that says which account each half named. The check applies to an access key and to an Entra client secret. A SAS token carries no account name, so the live probe alone decides whether it reaches this container.

Three fields an Azure destination refuses

Three of the S3 fields cannot be honoured on an Azure endpoint. Each is refused by name at save rather than accepted and left to fail on your first backup (AZURE_REFUSED_FIELDS in engine/src/dest/provider.ts, applied in engine/src/admin/router-destinations.ts). The refusal happens before the live probe, because the fault is in what was submitted and a round trip to the store could add nothing to it.

FieldWhy an Azure endpoint cannot honour itSet it where instead
Storage classAzure has access tiers (Hot, Cool, Cold and Archive) rather than Amazon’s storage-class names, and downpipes does no translation between the two vocabularies.Leave it blank, and set the access tier on the container or the storage account in Azure.
AssumeRoleSTS AssumeRole is an Amazon Web Services mechanism with no Azure equivalent, so a role ARN would never be assumed.Authenticate with the account name and one of its access keys, with a SAS token, or with an Entra service principal.
Addressing styleAddressing chooses whether the bucket sits in the request host or the path, which is an S3 concept. Azure Blob has one URL form, https://<account>.blob.core.windows.net/<container>/<blob>.Leave it unset.

Google Cloud refuses the first two, storage class and AssumeRole. Choosing Azure hides all three, and choosing Google Cloud hides its two, so you are not offered a control the save is certain to refuse. They are refused at the engine as well as hidden at the form: a value left behind by switching provider is not sent, and a destination configured any other way still returns a 400 naming the field and the remedy in the table above.

Immutability is not on that list. Azure enforces an immutability of its own, and the next section is how downpipes reaches it.

Azure immutability, and how the two modes map

Azure has immutability, not S3 Object Lock. downpipes writes Azure’s own primitive rather than sending an Amazon header Azure would reject (engine/src/dest/azure-worm.ts).

Azure carries two independent primitives. One is an immutability policy with a retain-until date, which is separately either unlocked or locked. The other is a legal hold, which holds a blob on its own account, with no expiry at all, until an administrator clears it. The form on this page collects one mode plus one retention window, which is the policy and not the hold.

The mapping onto that policy is exact rather than a guess, because the two vocabularies name the same two guarantees. governance means a sufficiently privileged principal can lift the retention, which is an unlocked Azure policy. compliance means nobody can shorten or remove it for the window, which is a locked one. Nothing has to be inferred from the container, because the mode rides on each write as x-ms-immutability-policy-mode with x-ms-immutability-policy-until-date beside it: compliance sends locked and either gets the strong guarantee or the write is refused. The legal hold is never set, because deriving an unexpiring hold from a retention-days answer would invent a promise you never made.

The precondition is Azure’s rather than ours. A per-blob policy only binds on a container with version-level immutability enabled, which you set on the storage account at creation or on the container. That container also needs blob versioning on the account. The engine probes for exactly that with one credentialed Get Container Properties read and takes the answer from x-ms-immutable-storage-with-versioning-enabled (objectLockStatus, engine/src/dest/azure-blob.ts), so a container without it is refused at save rather than stored and then failing every backup write. That is the same shape as an S3 bucket created without Object Lock.

The console offers the immutability control for an Azure destination, as it does for the other providers: choose the mode and the retention days on the Destinations screen. The container or the storage account must already have version-level immutability enabled in Azure (buildWormBlock, console/src/screens/destination-form-fields.ts). The engine’s destinations API (POST /admin/destinations, in admin endpoints) sets the same policy.

The three Azure credential kinds

Azure offers three kinds of credential, they are not interchangeable, and downpipes accepts all three. The engine picks between them from what is stored, so no two are ever armed at once.

CredentialWhat it isWhat to watch
Shared KeyThe storage account name and one of that account’s access keys, signed by the engine (signAzureSharedKey, engine/src/dest/azure-sharedkey.ts). It never expires.It is the whole storage account rather than a scoped grant, so treat it as an account-wide credential: keep the archive container in a storage account that holds nothing else, and rotate the key on whatever schedule your other destination credentials use.
SAS tokenA shared access signature you mint in Azure and paste where the access key goes. It carries its own signature, so the engine signs nothing and appends the token as query parameters. downpipes never mints one, because minting requires the account key.It dies on its own, at the instant written into it. The engine reads the se parameter out of the token so the expiry can be surfaced. A token with no se is reported as an unknown expiry rather than as one that never expires, because a service SAS can take its expiry from a stored access policy instead.
Entra service principalA tenant, an application id and a client secret, exchanged with Microsoft’s identity platform for a short-lived bearer token and authorised on the data plane by an Azure RBAC role assignment. No account key appears in the configuration at all, so the storage account can have its keys disabled.It is refused by name on the US Government and China clouds. Each cloud has its own login authority and its own storage scope, and the engine holds that pair for the commercial cloud only (AZURE_ENTRA_CLOUDS, engine/src/dest/azure-entra.ts). A Shared Key on the same account still works in every cloud.

The Shared Key and the SAS token both go in the credential pair the form already collects. The engine tells them apart by their structure. An Entra service principal needs the tenant and application id as well, and the console asks for both in a collapsed block below the credentials. The client secret goes in the credential box above it, because a destination carries exactly one secret and the client secret is it. Fill both ids or neither: a half-filled principal is refused at the form and again by the engine, rather than being quietly dropped.

Data Lake Storage Gen2 is not supported, and the Blob endpoint for the same account is

Azure Data Lake Storage Gen2 presents a storage account on a different host, https://<account>.dfs.core.windows.net, and that endpoint is a different protocol rather than another address for the Blob one. downpipes refuses it by name and says which store it recognised, so you are not sent to audit a credential that was never the problem (UNUSABLE_ENDPOINT_REASON, engine/src/dest/provider.ts). If the account holds ordinary block blobs, use its Blob endpoint instead: the same account name at blob.core.windows.net, which is supported.

The Azure US Government and China clouds use different endpoint suffixes, and the engine matches those too. core.usgovcloudapi.net and core.chinacloudapi.cn are both routed to the Azure client alongside the commercial core.windows.net (AZURE_STORAGE_SUFFIXES, engine/src/dest/provider.ts), so a storage account in either sovereign cloud is configured exactly as a commercial one is. The match is on a dot-prefixed suffix, so a host that merely ends in one of those strings cannot pass as an Azure endpoint. The one difference is the credential: an Entra service principal is refused on both sovereign clouds, and a Shared Key or a SAS token works in all three.

Region

For R2, auto is correct and is the default. For AWS S3 with static keys, the bucket’s real region is what you want. Under STS AssumeRole the region must be a real AWS region: the form refuses auto there, mirroring the engine’s own check, because the STS endpoint is regional.

Addressing style

Leave it on auto unless your store documents otherwise. For an AWS S3 host (one that ends in .amazonaws.com), auto picks virtual-hosted style when the bucket name has only lowercase letters, digits and hyphens. In every other case it picks path style, which is what almost every S3-compatible store expects. The explicit path and vhost options exist for the store that documents a firm requirement.

Storage class

Blank means the bucket’s default, which is right for R2. On S3, INTELLIGENT_TIERING is the cost-safe pick for archives: it moves cold objects to cheaper storage without changing retrieval behaviour. STANDARD_IA and ONEZONE_IA trade availability or retrieval cost for a lower storage price.

Glacier and Deep Archive are deliberately not offered. A backup you cannot restore for hours is a backup that fails you on the day you need it, so the archive-hostile classes are excluded rather than left as a trap.

Object lock (WORM)

Off is the default and is right until you have decided your ransomware stance. governance mode lets a suitably privileged principal override the lock; compliance mode makes every object undeletable by anyone for the retention window you set, which also blocks your own retention pruning for that window. Choosing either mode requires a retention period in days. At save the engine asks the store live, and it refuses a bucket whose store answers that it cannot enforce a lock.

Which store mechanism those two modes reach, and what the bucket or container must already have been created with, differs by provider. Cloudflare R2 enforces no lock on any bucket by any path, so a mode on an R2 destination is refused. The row for each provider is in destination providers compared.

Off is also correct for an AWS S3 bucket with an Object Lock default retention rule. That rule applies the window to each archive without a mode here. AWS then requires a checksum on every write, and the engine sends one. The mechanism is in a bucket default retention rule on AWS S3.

Credentials, and AssumeRole

Access keys are sent once for verification and never shown again; the stored view carries the host, bucket, and region only. On AWS you can leave the keys as the standing credential or supply a role ARN to assume instead: with an ARN set, the keys are used only to assume the role, and the optional external id is the cross-account confused-deputy guard you should set whenever the ARN crosses an account boundary.

The default destination

Your first destination becomes the default. A downpipe that does not pick destinations explicitly writes to the default, so repointing the default is a fleet-wide decision. Fanning a downpipe out to more than one destination is its own topic: see multiple destinations for the 3-2-1 pattern.

Last updated .