---
description: Restrict access to Cloudflare Images by generating signed URL tokens with expiration times.
title: Serve private images
image: https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/og.png?v=7299ee84ae1a2d1a
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/images/llms.txt  
> Use this file to discover all available pages before exploring further.

# Serve private images

Last updated Apr 21, 2026|Copy as Markdown| [View as Markdown](https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/index.md)| [Agent setup](https://developers.cloudflare.com/agent-setup/)

You can serve private images by using signed URL tokens. When an image requires a signed URL, the image cannot be accessed without a token unless it is being requested for a variant set to always allow public access.

1. In the Cloudflare dashboard, go to the **Hosted Images** page. [Go to **Hosted images** ↗](https://dash.cloudflare.com/?to=/:account/images/hosted)
2. Select **Keys**.
3. Copy your key and use it to generate an expiring tokenized URL.

Note

Private images do not currently support custom paths.

## Generate signed URLs from your backend

Signed URLs are generated server-side to protect your signing key. The example below uses a Cloudflare Worker, but the same signing logic can be implemented in any backend environment (Node.js, Python, PHP, Go, etc.).

The Worker accepts a regular Images URL and returns a signed URL that expires after one day. Adjust the `EXPIRATION` value to set a different expiry period.

Note

Never hardcode your signing key in source code. Store it as a secret using [`npx wrangler secret put`](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) and access it via the `env` parameter. For more information, refer to [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/).

```js
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer) =>
	[...new Uint8Array(buffer)]
		.map((x) => x.toString(16).padStart(2, "0"))
		.join("");

async function generateSignedUrl(url, signingKey) {
	// `url` is a full imagedelivery.net URL
	// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile

	const encoder = new TextEncoder();
	const secretKeyData = encoder.encode(signingKey);
	const key = await crypto.subtle.importKey(
		"raw",
		secretKeyData,
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"],
	);

	// Attach the expiration value to the URL
	const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
	url.searchParams.set("exp", expiry);
	// `url` now looks like
	// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

	const stringToSign = url.pathname + "?" + url.searchParams.toString();
	// e.g. /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

	// Generate the HMAC signature
	const mac = await crypto.subtle.sign(
		"HMAC",
		key,
		encoder.encode(stringToSign),
	);
	const sig = bufferToHex(new Uint8Array(mac).buffer);

	// Attach the signature to the URL
	url.searchParams.set("sig", sig);

	return new Response(url);
}

export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);
		const imageDeliveryURL = new URL(
			url.pathname
				.slice(1)
				.replace("https:/imagedelivery.net", "https://imagedelivery.net"),
		);
		// IMAGES_SIGNING_KEY is set via `npx wrangler secret put IMAGES_SIGNING_KEY`
		return generateSignedUrl(imageDeliveryURL, env.IMAGES_SIGNING_KEY);
	},
};
```

```ts
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer: ArrayBuffer) =>
	[...new Uint8Array(buffer)]
		.map((x) => x.toString(16).padStart(2, "0"))
		.join("");

async function generateSignedUrl(
	url: URL,
	signingKey: string,
): Promise<Response> {
	// `url` is a full imagedelivery.net URL
	// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile

	const encoder = new TextEncoder();
	const secretKeyData = encoder.encode(signingKey);
	const key = await crypto.subtle.importKey(
		"raw",
		secretKeyData,
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"],
	);

	// Attach the expiration value to the URL
	const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
	url.searchParams.set("exp", expiry);
	// `url` now looks like
	// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

	const stringToSign = url.pathname + "?" + url.searchParams.toString();
	// e.g. /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

	// Generate the HMAC signature
	const mac = await crypto.subtle.sign(
		"HMAC",
		key,
		encoder.encode(stringToSign),
	);
	const sig = bufferToHex(new Uint8Array(mac).buffer);

	// Attach the signature to the URL
	url.searchParams.set("sig", sig);

	return new Response(url);
}

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const url = new URL(request.url);
		const imageDeliveryURL = new URL(
			url.pathname
				.slice(1)
				.replace("https:/imagedelivery.net", "https://imagedelivery.net"),
		);
		// IMAGES_SIGNING_KEY is set via `npx wrangler secret put IMAGES_SIGNING_KEY`
		return generateSignedUrl(imageDeliveryURL, env.IMAGES_SIGNING_KEY);
	},
} satisfies ExportedHandler<Env>;
```

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/#page","headline":"Serve private images","description":"Restrict access to Cloudflare Images by generating signed URL tokens with expiration times.","url":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/","inLanguage":"en","image":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/og.png?v=7299ee84ae1a2d1a","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
