
The Cloudflare ecosystem, with its powerful foundational services and generous free tier, is becoming the cornerstone for many developers building web applications. From R2 object storage and Workers compute to Pages static site hosting, Cloudflare offers a one-stop solution that significantly lowers the barrier to entry and subsequent operational overhead, especially for projects just starting out.
Our team’s previous projects typically used Vue3 for the frontend, which was compiled and deployed directly to Cloudflare Pages. Paired with custom domain configuration, this not only easily provided HTTPS and CDN acceleration but also made subsequent maintenance a virtually ‘zero-touch’ experience.
Recently, while designing and developing an AIGC (AI Generated Content - image-to-image, text-to-image) web application based on the OpenAI API, we faced a core requirement: how to securely, reliably, and cost-effectively store user-uploaded and AI-generated images for the long term? The answer was almost immediately clear: Cloudflare R2.
Cloudflare R2 Free tier:
Free | |
---|---|
Storage | 10 GB-month / month |
Class A Operations | 1 million requests / month |
Class B Operations | 10 million requests / month |
Egress (data transfer to Internet) | Free |
- Class A Operations: Typically involve write or modify actions (e.g., PutObject, CopyObject).
- Class B Operations: Typically involve read actions (e.g., GetObject).
The free egress is particularly appealing. This means when users access resources (like images) stored on R2, we don’t have to worry about high bandwidth costs.
R2 supports connecting custom domains. The simplest method is to point your domain (or a subdomain like images.yourdomain.com
) via a CNAME record to the public R2 bucket URL provided (e.g., public.your-bucket.your-account-id.r2.cloudflarestorage.com
). Once configured, you can access R2 resources using URLs like https://images.yourdomain.com/your-object.jpg
.
However, convenience often comes with risks. This direct CNAME approach essentially leaves your R2 bucket exposed on the public internet without any permission validation. Anyone who knows your domain and the object key (filename) can directly access, and potentially misuse or hotlink, your resources. For our AIGC application, user-uploaded and AI-generated images often have privacy implications, making strict access control essential. Measures like hotlink protection, authentication, and time-limited access are indispensable.
So, how can we enjoy the convenience of R2 custom domains while ensuring the security of our resources?
Option 1: Presigned URLs - Secure but Limited
Cloudflare R2 is compatible with the AWS S3 API. This means we can leverage the Cloudflare API or compatible S3 SDKs (such as the AWS SDK for Go, Java, Python, etc.) to generate temporary access URLs for specific objects within a bucket. These URLs contain signed credentials and have a defined expiration time.
The process generally looks like this:
- Frontend Request: The frontend page needs to load a specific image.
- Backend Signature Generation: The frontend sends a request to the backend service (e.g., a RESTful API implemented in Go), specifying which image needs to be accessed.
- Backend Maps to R2 Object: The backend service identifies the corresponding R2 object based on the request.
- Backend Signs the URL: The backend service uses R2 access credentials (Access Key ID and Secret Access Key) to generate a presigned URL for that object. This URL includes the access credentials and a set expiration time (e.g., expires in 5 minutes).
- Return URL: The backend service returns the generated presigned URL to the frontend.
- Frontend Access: The frontend uses this temporary, signed URL to request the R2 resource, typically via an HTML
<img>
tag or other methods.
Pros:
- High Security: Each URL is temporary, includes access credentials and an expiration time, effectively preventing unauthorized access and resource theft/hotlinking.
- Fine-grained Control: Allows generating unique access credentials for each object and request.
Cons:
- Cannot Use Custom Domain: The generated presigned URLs enforce the use of R2’s original domain (
your-bucket.your-account-id.r2.cloudflarestorage.com
). If you want users to access resources via your own branded domain (e.g.,images.mydomain.com
) and hide the underlying R2 bucket details, this method doesn’t meet the requirement.
For scenarios demanding brand consistency and concealing the underlying storage implementation, presigned URLs are clearly not the ideal solution.
Option 2: Cloudflare Workers - Custom Domain Access Control
Since presigned URLs fall short for custom domain requirements, is there a way to achieve both convenience and control? Absolutely: Cloudflare Workers.
Cloudflare Workers allow us to run JavaScript code directly on Cloudflare’s edge network, enabling us to intercept and manipulate HTTP requests.
We can configure our custom domain (e.g., images.mydomain.com
) to route traffic through a deployed Cloudflare Worker. This Worker functions as an intelligent “gatekeeper,” responsible for:
- Receiving all incoming requests for R2 resources via the custom domain.
- Validating the request’s authenticity (e.g., checking tokens, verifying origin, evaluating permissions).
- If the request is valid, the Worker internally fetches the requested resource from the actual R2 bucket.
- Streaming the data retrieved from R2 back to the original requesting client.
This approach ensures that the public-facing endpoint is always our custom domain, while the underlying R2 access logic and permission controls are securely encapsulated within the Worker, perfectly addressing the limitations of presigned URLs.
Strategy A: Token Authentication via HTTP Header (JWT-like)
This was the first strategy I considered, drawing inspiration from common API authentication practices.
Implementation Flow:
- Domain Routing: Ensure the custom domain
images.mydomain.com
is pointed to the deployed Cloudflare Worker. - Frontend Resource Request: The frontend needs to access
image001.png
stored in R2. - Obtain Token: The frontend first obtains a time-limited authentication token (similar to a JWT, potentially containing user identity, permissions, expiration time, etc.) from a backend service or a dedicated authorization service.
- Request with Token: When requesting
https://images.mydomain.com/image001.png
, the frontend includes the obtained token in an HTTP header, typicallyAuthorization: Bearer <your_token>
. - Worker Interception & Validation: The Cloudflare Worker intercepts the request and extracts the token from the
Authorization
header. - Worker Verification: The Worker validates the token’s signature, checks its expiration, parses embedded permission information, etc.
- Worker Proxies to R2: Upon successful validation, the Worker internally constructs a request to fetch
image001.png
from the R2 bucket (requires configuring the Worker with permissions to access the R2 bucket) and retrieves the data. - Worker Returns Response: The Worker creates a new HTTP Response and streams the data fetched from R2 back to the frontend.
Problem Encountered:
This strategy was sound in terms of logic and Worker implementation. However, a significant hurdle emerged during frontend integration: the limitations of the browser’s native <img>
tag.
In the project, images are typically loaded directly using the standard HTML <img>
tag:
<img src="https://images.mydomain.com/image001.png" alt="Image">
The browser, when fetching the resource specified in an <img>
tag’s src
attribute, generally does not provide a straightforward way to add custom HTTP headers like Authorization
, unlike when using fetch
or XMLHttpRequest
.
This implies that sticking with header-based token transmission would necessitate a major overhaul of how images are loaded on the frontend:
- We could no longer directly use the
src
attribute of the<img>
tag. - We would need to use the
fetch
API to initiate the request, manually adding theAuthorization
header. - The response body returned by
fetch
would need to be converted into a Blob object. URL.createObjectURL()
would then be used to generate a temporary local URL for this Blob.- Finally, this generated Blob URL would be assigned to the
src
attribute of the<img>
element.
// Example: Loading an image with a token using fetch
const imageUrl = 'https://images.mydomain.com/image001.png';
const token = 'your_bearer_token';
fetch(imageUrl, {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob(); // Get response body as a Blob
})
.then(blob => {
const objectURL = URL.createObjectURL(blob); // Create a temporary URL
const imgElement = document.getElementById('myImageElement'); // Get the <img> element
imgElement.src = objectURL; // Set the src to the Blob URL
// Optional: Revoke the object URL when no longer needed to free memory
// imgElement.onload = () => URL.revokeObjectURL(objectURL);
})
.catch(error => console.error('Error loading image:', error));
Although technically possible, this approach significantly increases frontend complexity, deviates from the standard way of loading images, and poses compatibility challenges for existing code.
Strategy B: Token Authentication via URL Query Parameter
To preserve the simplicity of using the standard <img>
tag for loading images, I revised the strategy, moving the authentication token from the HTTP header into a URL query parameter.
Implementation Flow:
- Domain Routing: Similarly, the custom domain
images.mydomain.com
points to the Cloudflare Worker. - Frontend Resource Request: The frontend needs to access
image001.png
on R2. - Obtain Tokenized URL: The frontend requests the image access URL from the backend service. The backend generates a time-limited token and appends it as a query parameter to the image URL, resulting in a format like
https://images.mydomain.com/image001.png?token=xxxxxxx
. - Backend Returns URL: The backend returns this complete URL (with the token) to the frontend.
- Frontend Uses URL Directly: The frontend receives this URL and can directly assign it to the
src
attribute of an<img>
tag. - Worker Interception & Validation: The Cloudflare Worker intercepts the request and extracts the
token
from the URL’s query parameters. - Worker Verification: The Worker validates the token’s authenticity (decryption, signature check, expiration check, etc.).
- Worker Proxies to R2: Upon successful validation, the Worker ignores the query parameters and uses the path (
/image001.png
) to fetch the object data from the bound R2 bucket. - Worker Returns Response: The Worker streams the data retrieved from R2 back to the frontend.
<img src="https://images.mydomain.com/image001.png?token=xxxxxxx" alt="Image">
Pros:
- Frontend-Friendly: Minimally invasive to frontend code; allows continued use of standard
<img>
tags without changing image loading logic. - Simpler Implementation: The overall flow is relatively straightforward.
Potential Cons:
- Token Exposure in URL: The token appears in the browser’s address bar, server logs, Referrer headers, etc., making it slightly less secure than header transmission. However, using short token validity periods (e.g., a few minutes) can effectively mitigate this risk.
Considering our project’s emphasis on frontend simplicity and the fact that a short token lifetime provided an acceptable security model for our use case, we ultimately opted for the token authentication scheme based on URL query parameters.
Summary
Cloudflare R2 provides a highly compelling object storage service, particularly attractive due to its generous free tier and zero-cost egress policy. However, simply connecting a custom domain via CNAME for public access introduces significant security vulnerabilities.
To achieve secure access control for R2 custom domains, we explored two primary strategies:
- Presigned URLs: Offer high security and granular control but cannot be used directly with a custom domain, exposing the native R2 endpoint.
- Cloudflare Workers as a Proxy: Enables the perfect combination of custom domains and access control.
- Authentication via Headers: Aligns well with API security practices but is incompatible with simple
<img>
tag usage, necessitating complex frontend workarounds. - Authentication via URL Parameters: Extremely frontend-friendly, compatible with standard
<img>
tags, and simpler to implement. The main consideration is managing token lifetimes to minimize potential exposure risks.
Ultimately, prioritizing frontend ease-of-use, we implemented the solution using Cloudflare Workers combined with token authentication via URL parameters. This allowed us to successfully build an R2 resource serving system that leverages our custom domain while enforcing granular access control.
This project reaffirmed the power and flexibility derived from combining components within the Cloudflare ecosystem, specifically R2 and Workers.