What a signed URL actually promises
Three things, and it is worth being precise because people expect a fourth.
It proves the link came from you. The signature is a hash of the path, the expiry and a secret only your server knows. Nobody can fabricate a working link without the secret.
It expires. The expiry is part of what is signed, so it cannot be edited without breaking the signature.
It is checked before your origin is touched. At a CDN edge, an unsigned request is refused at the edge; your server never sees it.
What it does not promise: that the person who opened the link will not save the file and email it to a friend. A signed URL controls access to the URL, not to the bytes. If you need control after the download, you are looking for DRM, which is a different and much heavier product.
Two families: edge signatures and presigned URLs
Path signatures at the edge — what a CDN gives you. Your application computes a signature for a path and an expiry; the edge verifies it on arrival. The file can live anywhere the CDN can reach, and the check happens close to the visitor.
Presigned URLs from object storage — what S3 and S3-compatible storage give you. The storage service itself issues a temporary URL, signed with your access key, usually valid for minutes. The signature travels in the query string as X-Amz-Signature and friends.
They are not competitors. A common, sane arrangement is object storage behind a CDN, with the edge signature protecting the public URL and the bucket kept private so nobody can bypass the edge. What you should not do is expose a presigned storage URL directly to the public and assume the CDN is protecting it — if the link points at the bucket, the edge is not in the path at all.
How it works on cdn.com.tr
It is a setting on a delivery rule, so you can protect /downloads while the rest of the site stays public. Turn on expiring links for that rule, set a secret, and generate links in your application.
The signature is the base64url form of the raw MD5 of three things joined together: the expiry, the path, and the secret with a single space before it.
``` $uri = "/downloads/report.pdf"; $expires = time() + 600; // ten minutes $secret = "your-long-random-secret";
$token = rtrim(strtr(base64_encode( md5($expires . $uri . " " . $secret, true) ), "+/", "-_"), "=");
$url = "https://cdn.example.com{$uri}?md5={$token}&expires={$expires}"; ```
Three outcomes, and they are deliberately distinct so your logs can tell them apart:
| Request | Response | |---|---| | Valid signature, not expired | the file, cached normally | | No signature, or a wrong one | 403 | | Correct signature, expiry passed | 410 |
The 410 matters more than it looks. When a customer says "your link is broken", the status code alone tells you whether they were sent a bad link or simply waited too long — without asking them for anything.
The cache trap that costs you the CDN
This is the part that is missing from most documentation, and it is the one that hurts.
Every signed link is unique: a different md5 and a different expires for every visitor and every issue. If your cache key includes the query string — which is the common default, and is ours unless you change it — then each signed request is a distinct cache entry. The hit ratio for protected files goes to zero, every download is fetched from your origin, and you have paid for a CDN that is acting as a proxy.
The fix is one setting. On the delivery rule, set the query-string mode so the signature parameters do not enter the cache key:
- Ignore list — ignore md5 and expires, keep anything else meaningful. This is the right choice when the same file is also fetched with real parameters.
- Ignore all — the simplest option when the protected paths take no meaningful parameters at all, which is usually true for downloads.
Check it afterwards rather than assuming: request the same file twice with two different valid signatures and look at the cache status header. The second one should be a hit. If both are misses, the key still contains the signature.
Choosing an expiry
Short enough that a leaked link is worthless; long enough that the download finishes on a bad connection.
Documents and images: five to fifteen minutes. They are fetched immediately after the click.
Large video and archives: one to six hours. A two-gigabyte file on a slow mobile connection takes longer than people expect, and an expiry that fires mid-download produces a support ticket that looks like corruption.
Streaming manifests need care. With HLS, the player fetches the manifest and then many segments over the duration of playback. If you sign each segment with a short expiry, playback breaks partway through a long video. Sign with an expiry that covers the whole session, or sign the manifest and leave segments protected by a different control.
One thing you cannot do is revoke a single issued link. It is valid until its expiry, full stop. If you need immediate revocation, rotate the secret — which invalidates every link you have issued.
The mistakes that leak the secret
Signing in the browser. If the secret is in JavaScript, it is public. Signing belongs on the server, always. This sounds obvious and it is the single most common way secrets escape.
Baking links at build time. A link generated during a static build carries an expiry set at build time, and it expires while the page is still live. Generate at request time, or through a small endpoint that redirects.
Clock skew. The expiry is compared against the edge clock. If your application server is minutes behind, links are born expired. Keep NTP running; when a link fails immediately after issue, check the clock before the code.
Putting the secret in a repository. Treat it as a credential: environment variable, not source control. When someone leaves the team, rotate it.
Forgetting that rotation is global. Changing the secret invalidates every outstanding link at once, including the ones in emails sent five minutes ago. That is exactly what you want during an incident and exactly what you do not want on a Friday afternoon by accident.
Signed URL FAQ
What is the difference between a signed URL and a presigned URL?
Mostly where the check happens. "Presigned URL" is the S3 term for a temporary link issued by the storage service and verified by it. A signed URL at a CDN edge is verified at the edge, close to the visitor, and works for any origin rather than just a bucket. The concept is the same: a signature plus an expiry in the query string.
Can someone share a signed link?
Yes, until it expires. A signature proves the link was issued by you; it says nothing about who is holding it. Short expiries limit the damage. If you need the link tied to one person, include something identifying in the signed path and check it on your side, and accept that a determined sharer can still forward the downloaded file.
Does signing hurt caching?
Only if you let the signature into the cache key, and then it hurts badly — every unique link becomes its own cache entry and every download goes to your origin. Configure the query-string mode to ignore the signature parameters and the file caches normally, shared across everyone who holds a valid link.
Can I revoke one link?
Not individually. A signature is valid until its expiry because verification is stateless — there is no list to remove it from. The available controls are a short expiry and rotating the secret, which invalidates everything at once.
Is MD5 not broken for this?
MD5 is broken for collision resistance, which matters when an attacker can choose both messages. Here the attacker must forge a hash of a string containing a secret they do not have, which is a preimage problem rather than a collision one. The practical risk is the secret leaking or being guessable — use a long random one, keep it server-side, and rotate it.
Should I use this for HLS video?
It works, with the caveat about session length: the expiry has to cover the whole playback, not just the manifest request, or playback stops partway. For long content, pair a generous expiry with rate limiting rather than a very short expiry that breaks the player.