CDN S3 Origin Authentication — Pull from Private Buckets

If your origin is an object store (AWS S3, Cloudflare R2, Wasabi, Backblaze B2, MinIO, etc.) and the bucket is private, the CDN needs to authenticate every pull request with AWS Signature Version 4 (SigV4). With S3 Origin Authentication you give the CDN your bucket credentials once, and from then on every cache miss is signed transparently — visitors never see the credentials, your bucket stays private, and you don't have to publish it to make it work.

This guide explains when to use it, how to configure it, and how to avoid the most common 403 / 404 / 421 you'll hit on the first try.

When you need this

Your originDo you need this?
Your own web server (Nginx, Apache, custom API)No — origin serves over plain HTTPS, no signing required
Public S3-compatible bucket (you've enabled public access)No — anyone can fetch with a plain GET
Private S3-compatible bucket (read requires AWS signature)Yes — without this every fetch gets 403

Supported providers

Any object store that speaks AWS SigV4 over an S3 API works out of the box:

  • AWS S3 — endpoint s3.<region>.amazonaws.com
  • Cloudflare R2 — endpoint <account_id>.r2.cloudflarestorage.com (or <account_id>.eu.r2.cloudflarestorage.com for EU jurisdiction)
  • Wasabi — endpoint s3.<region>.wasabisys.com
  • Backblaze B2 (S3-compatible API) — endpoint s3.<region>.backblazeb2.com
  • MinIO (self-hosted) — your own endpoint
  • Any other S3-compatible — provide the endpoint URL

Configuring the origin

Add or edit an origin on your CDN zone: dashboard → Origins tab → Add Origin (or pencil icon to edit).

The first part is the same as any other origin:

FieldValue
NameFree text. Anything that helps you recognize this origin.
Origin URLhttps://<your-endpoint> (no trailing path). Example: https://s3.eu-central-1.amazonaws.com
Host HeaderUsually leave blank — auto-filled with the endpoint hostname. Don't set it to your CDN domain.
Base path/<bucket-name> if your customer-facing URLs don't include the bucket name. See the section below.

Then expand the Private bucket (S3 SigV4) section at the bottom and fill:

FieldValue
Enable SigV4 signingToggle on
Access key IDThe S3-style access key (looks like an ID — safe to display)
Secret access keyThe S3-style secret. Write-only — once saved, we don't display it again
RegionProvider-specific. For R2 leave blank (defaults to auto). For AWS / Wasabi / Backblaze use the bucket's region (us-east-1, eu-central-1, etc.)
Services3 (default — works for all the supported providers)

Save. Within a few seconds the CDN propagates the new credentials to every PoP and starts signing requests transparently.

Path mapping (the most common gotcha)

The first segment of the URL path is what S3 considers the bucket name. So if your CDN URL is:

https://your-zone.cubecdn.io/videos/clip.mp4

The CDN forwards /videos/clip.mp4 to your origin. S3 reads:

  • bucket = videos
  • key (object path inside the bucket) = clip.mp4

If you actually have your files under bucket my-media, the request looks for bucket videos which doesn't exist → 403 / 404 / "InvalidBucketName".

You have two ways to fix this:

Option A — Use the bucket name as the first segment of your customer URLs

Cleanest. If your bucket is my-media, customers request:

https://your-zone.cubecdn.io/my-media/path/to/file.mp4

Leave Base path empty in the origin. Files like path/to/file.mp4 will be read from bucket my-media. This works if you don't mind exposing the bucket name in your URLs.

Option B — Hide the bucket name with Base path

If you want shorter URLs (no bucket name visible to customers), set:

  • Base path = /my-media

Then customers request:

https://your-zone.cubecdn.io/path/to/file.mp4

And the CDN rewrites the path to /my-media/path/to/file.mp4 before forwarding to S3. The bucket name never appears in customer URLs.

Choosing the right token / API key permissions

When you create the credentials on your S3 provider, give them the minimum permissions needed:

  • Object Read — sufficient if the CDN only fetches existing objects (this is the common case)
  • Object Read & Write — only if your application also uses the same credentials for uploads via something else
  • Admin — avoid

And scope the credentials to only the bucket(s) you're serving from this CDN zone. On Cloudflare R2 use "Specify bucket" and pick the relevant bucket; on AWS use an IAM policy with Resource: "arn:aws:s3:::my-media/*". Don't give it access to your whole account.

Updating credentials (rotation)

When you regenerate the S3 credentials at the provider (security best practice every few months), update them in the dashboard the same way you set them up:

  1. Open the origin → edit → expand Private bucket (S3 SigV4) section
  2. Replace the Access key ID with the new one
  3. Paste the new Secret access key. Leaving this field blank keeps the current one — only fill it if you're rotating
  4. Save

We propagate the new credentials within ~30 seconds. Cached responses from before rotation keep being served from cache as long as their TTL is valid — only fresh fetches use the new credentials.

Cloudflare R2 jurisdictions

R2 has two jurisdictions: default and EU. Buckets are created in one or the other, and the endpoint URL is different:

  • Default: <account_id>.r2.cloudflarestorage.com
  • EU: <account_id>.eu.r2.cloudflarestorage.com

If you point the CDN at the wrong jurisdiction, R2 returns 421 Misdirected Request even with correct credentials. Check your bucket's jurisdiction in the Cloudflare dashboard (R2 → your bucket → Settings → Location) and use the matching endpoint.

Also: API tokens are scoped to a jurisdiction. A token created from the default R2 view doesn't work against EU buckets and vice versa. If you migrate a bucket between jurisdictions, regenerate the token too.

Common errors and what they mean

Status / CodeMeaningFix
403 AccessDenied (XML body)S3 received a valid signature but the credentials don't have permission on this bucket/objectCheck the bucket scope of the token and the resource permissions
403 SignatureDoesNotMatchThe signature math itself is wrongRe-enter the secret access key; check the region is correct for the bucket
400 InvalidArgument: AuthorizationRequest reached S3 without auth — credentials were missing entirelyRe-save the origin with credentials; verify SigV4 toggle is on
404 NoSuchBucketThe first path segment doesn't match any bucket in your accountSee "Path mapping" above
404 NoSuchKeyBucket is correct but the object doesn't exist at that pathVerify the exact key in your bucket — case matters
421 Misdirected Request (Cloudflare R2)Wrong jurisdiction endpoint, or wrong account ID in the URLSee "Cloudflare R2 jurisdictions" above
503 Service UnavailableOrigin unreachable from the edgeCheck your provider's status page; verify the endpoint URL

The XML body in the response usually tells you the exact <Code> and <Message>. If you only get an HTML response, that means the error came from a layer in front of S3 (typically a routing / SNI mismatch — see the 421 case).

Constraints

  • All S3-authenticated origins within the same zone must share the same credentials. The CDN signs per backend, not per server, so if you list two origins both with SigV4 enabled they need identical access key + region + endpoint. For separate buckets with separate credentials, create separate CDN zones.
  • The secret access key is write-only. After you save the origin we don't display it again. If you forget it, regenerate at the provider and re-enter.
  • GET / HEAD requests only. The CDN doesn't sign PUT/POST/DELETE — it's a delivery network, not an upload pipeline.

API reference

POST   /cdn/zones/{uuid}/origins                     create origin
PATCH  /cdn/zones/{uuid}/origins/{origin_uuid}       update origin

Body fields for both:
{
  "s3_auth_enabled": true|false,
  "s3_access_key":   "AKIAXXXXXXXXXXXXX",
  "s3_secret_key":   "<write-only, leave blank to keep current on PATCH>",
  "s3_region":       "us-east-1"  // or empty for R2 (defaults to "auto")
}

s3_secret_key blank on PATCH = keep the persisted value. It's never returned in any GET response.