Articles

Presigned URLs Explained: Secure Temporary File Access

A presigned URL grants time-limited access to a private cloud storage object without sharing credentials. How S3-style presigned URLs work.

Chisato Chisato · · 5 min read
A set of house keys held in a hand

A presigned URL is a link to a private object in cloud storage that carries its own temporary authorization. Your server signs the URL using its credentials, embeds an expiry time, and hands it to a client. Until the URL expires, anyone holding it can perform exactly the operation it was signed for, such as downloading one file or uploading to one key, without ever seeing your credentials. Amazon S3 popularized the pattern, and S3-compatible stores and other providers offer the same idea, sometimes under the name signed URLs.

The problem presigned URLs solve

Files in object storage are usually private. Making a bucket public is a frequent source of data leaks, and putting cloud credentials in a browser or mobile app is never acceptable. That leaves two traditional options for serving private files:

  1. Proxy everything through your server. The app server authenticates the user, downloads the object from storage, and streams it back. This works, but your servers now carry every byte of every file, which costs bandwidth, memory, and latency.
  2. Make objects public with obscure names. Cheap and fast, but anyone who learns the URL has permanent access, and there is no way to revoke it short of renaming the object.

Presigned URLs offer a third path. Your server still decides who may access what, but the actual transfer goes directly between the client and the storage service.

How a presigned URL works

Generating a presigned URL does not involve a network call to the storage service. It is a local cryptographic operation.

  1. Your server builds a canonical description of the request: the HTTP method, the bucket and object key, selected headers, and an expiry.
  2. It computes a signature over that description using a key derived from its secret credentials. For S3, this is Signature Version 4, which uses HMAC with SHA-256.
  3. The signature, the access key ID, the timestamp, and the expiry are appended to the URL as query parameters.

A presigned download URL looks roughly like this, shortened:

https://my-bucket.s3.amazonaws.com/reports/q3.pdf
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=AKIA.../us-east-1/s3/aws4_request
  &X-Amz-Date=20261024T120000Z
  &X-Amz-Expires=900
  &X-Amz-SignedHeaders=host
  &X-Amz-Signature=3f9a...

When the request arrives, the storage service recomputes the signature using its copy of the secret, checks that it matches, checks that the URL has not expired, and checks that the signing identity has permission for the action. If all three pass, the request proceeds as if the signer made it.

Because the signature covers the method and the object key, a client cannot change GET to DELETE or swap in a different file. Any modification invalidates the signature.

Common use cases

Private downloads. A user clicks “download invoice.” Your API checks they own the invoice, generates a presigned GET URL valid for a few minutes, and redirects the browser to it. The file streams directly from storage.

Direct uploads from the browser. Instead of sending a large video through your API, the client asks your server for a presigned PUT URL for a specific key, then uploads straight to the bucket. Your server never touches the bytes. For browser uploads, the bucket needs a CORS policy that allows your site’s origin and the PUT method.

Sharing with third parties. A presigned link lets a partner or an external service fetch a file once without being given any long-lived credentials.

Serving media behind a CDN. Some setups combine presigned or signed URLs with a CDN so cached content still requires a valid, time-limited link. CDNs typically have their own signed URL or signed cookie mechanism for this.

A typical upload flow

// Server: user is authenticated and allowed to upload an avatar
const key = `avatars/${user.id}/${crypto.randomUUID()}.png`;
const url = await getSignedUrl(
  s3,
  new PutObjectCommand({ Bucket: "app-uploads", Key: key, ContentType: "image/png" }),
  { expiresIn: 300 }
);
return { url, key };

// Client
await fetch(url, { method: "PUT", headers: { "Content-Type": "image/png" }, body: file });

The server chooses the key, so a client cannot overwrite another user’s files. Signing the content type means the upload must declare that type to be accepted. After the upload, the client tells your API the key, and your server records it.

For tighter control over uploads, S3 also supports presigned POST, which attaches a policy document with conditions such as a maximum file size and an allowed key prefix. A plain presigned PUT cannot enforce a size limit on its own.

Security considerations

  • Keep expiry short. Minutes, not days. A presigned URL is a bearer token: whoever has it can use it, and it can leak through browser history, logs, referrer headers, or a forwarded message.
  • Revocation is limited. You cannot invalidate a single presigned URL. The practical levers are short expiry, deleting or moving the object, or rotating or revoking the credentials that signed it, which invalidates every URL that identity signed.
  • Sign with a narrowly scoped identity. The URL can do only what the signer is allowed to do. Generate URLs with a role that has access to just the relevant bucket and prefix, following the principle of least privilege.
  • Watch temporary credentials. If the signer is using short-lived session credentials, the URL stops working when those credentials expire, even if the URL’s own expiry is later.
  • Validate after upload. A client can still upload malicious or mislabeled content within the rules you signed. Scan and validate files before processing or serving them to other users.
  • Avoid logging full URLs. Strip query strings from access logs and analytics where possible so signatures are not retained.

Presigned URLs compared with alternatives

ApproachServer bandwidthCredentials exposedExpiryFine-grained per request
Proxy through your serverHighNoneN/AYes
Public bucketNoneNoneNeverNo
Presigned URLNoneNone (signature only)Built inYes
Temporary credentials to clientNoneScoped, short-lived keysBuilt inCoarser, policy-based

The takeaway

A presigned URL is a storage request your server has pre-authorized and signed, with an expiry baked in. It lets clients download or upload private objects directly, keeping file traffic off your servers and credentials out of the client. Keep expiry short, sign with narrowly scoped identities, let the server choose object keys, use presigned POST when you need size limits, and remember that a leaked URL works for anyone until it expires.