ZeroBuffer
PricingDocsNetworkBlogsContact
Sign inStart free
ZeroBuffer
PricingDocsNetworkBlogsContact
Sign inStart free
  1. Home
  2. /
  3. Blogs
Secure Content Delivery & Access Control

S3 Presigned URLs: Secure, Expiring Access to Media Files

Learn how an S3 presigned URL works, generate secure upload and download links, set expiration, prevent abuse, and choose signed CDN delivery for media.

By Sahil AsopaAugust 18, 202615 min read3,391 wordsView as Markdown
A private object store issues a short-lived signed path for secure file transfer
A private object store issues a short-lived signed path for secure file transfer

Your application needs to accept a 4 GB video or release a private report, but routing those bytes through the API server wastes bandwidth and turns every transfer into backend load. Making the bucket public avoids that bottleneck by creating a much worse problem.

An S3 presigned URL is a time-limited URL that authorizes one specific object-storage operation, such as downloading one key with GET or uploading it with PUT. Your backend authenticates the user and signs the request; the client then transfers the file directly with the private bucket, without receiving cloud credentials.

That is the useful part. The dangerous part is treating the URL as a complete access-control system. It is a bearer token: anyone who obtains it can use its delegated permission until the signature or signing credentials expire. A production design therefore has to control who may request a URL, which object key and method it covers, how long it lasts, what an upload may contain, and what happens after the transfer.

This guide builds that design, with JavaScript and Python examples, a security checklist, and a clear boundary between direct object-store access and signed CDN delivery.

How an S3 presigned URL works

Normally, a private S3 request needs an AWS identity with permission to perform an action. A presigner uses its own credentials to create a Signature Version 4 request whose authentication values live in the query string. The client does not learn the secret key.

The signed URL normally includes:

  • X-Amz-Algorithm, usually AWS4-HMAC-SHA256;
  • X-Amz-Credential, which identifies the access key and signing scope;
  • X-Amz-Date, the signature timestamp;
  • X-Amz-Expires, the allowed lifetime in seconds;
  • X-Amz-SignedHeaders, the headers covered by the signature;
  • X-Amz-Signature, the computed signature; and
  • X-Amz-Security-Token when temporary credentials are used.

S3 reconstructs the expected signature from the request. A changed key, method, signed header, region, expiration, or query value no longer matches, so the service rejects the request. AWS documents the parameter format and the one-second to seven-day SigV4 range in its query-string authentication reference.

A typical download flow is short:

  1. The user asks your API for tenant-42/reports/august.pdf.
  2. Your API authenticates the session and verifies that this user owns that report.
  3. A narrowly permitted backend role signs GetObject for that exact bucket and key.
  4. Your API returns the URL, usually with its expiry time.
  5. The client sends GET directly to S3.
  6. S3 validates the signature and streams the object.

Uploads use the same handoff with a write operation. The client first sends metadata to your application, the application allocates an object key and authorizes the upload, and then the file travels directly to object storage. Presigning removes your API from the data path; it does not remove your API from the authorization path.

The effective expiration is the shortest limit

X-Amz-Expires is only one clock. A URL signed with temporary role credentials stops working when those credentials expire, even if the requested URL lifetime is longer. Revoking, deactivating, or deleting the signing credentials can also invalidate a URL early. AWS explains these interactions in its presigned URL user guide.

S3 checks expiration when a request begins. A download that starts just before expiry can continue over the existing connection, but a retry after expiry fails. That matters for large objects and unreliable mobile networks: make the window long enough for the request to start after realistic authentication, queue, and network delays, but do not stretch it to cover the entire theoretical transfer time.

A presigned URL is reusable

An ordinary presigned URL is not a one-time token. It can be replayed until it expires, subject to the signer's current permissions and any bucket-policy conditions. If a PUT targets an existing key, S3 can replace that object. Generate collision-resistant, server-owned keys instead of trusting a user-supplied filename, and make one-time business semantics a separate application state transition.

Generate an S3 presigned URL for downloads

Generate URLs only on a trusted server. The examples below rely on the SDK credential chain, so production credentials can come from an IAM role rather than source code or browser JavaScript.

JavaScript with AWS SDK v3

Install the S3 client and request presigner:

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Then sign a GetObjectCommand:

import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "ap-south-1" });

export async function createDownloadUrl(key: string) {
  const command = new GetObjectCommand({
    Bucket: "private-media",
    Key: key,
    ResponseContentDisposition: 'attachment; filename="download.mp4"',
  });

  return getSignedUrl(s3, command, { expiresIn: 300 });
}

This AWS presigned URL lasts five minutes unless the credentials expire sooner. The content-disposition override is part of the signed request, so altering it later breaks the signature. AWS maintains a fuller upload and download implementation in its SDK for JavaScript v3 examples.

Do not accept key directly from a query parameter and sign it. Resolve an application resource ID to an object key after authorization:

app.post("/api/downloads/:assetId", async (req, res) => {
  const user = requireUser(req);
  const asset = await assets.findById(req.params.assetId);

  if (!asset || asset.tenantId !== user.tenantId) {
    return res.sendStatus(404);
  }

  const url = await createDownloadUrl(asset.storageKey);
  res.json({ url, expiresAt: new Date(Date.now() + 300_000).toISOString() });
});

Returning 404 for both missing and unauthorized records avoids revealing which asset IDs exist. The important control is not the status code, though; it is deriving storageKey from an authorized database record rather than from client input.

Python with Boto3

Boto3 exposes the same model through generate_presigned_url:

import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    region_name="ap-south-1",
    config=Config(signature_version="s3v4"),
)

def create_download_url(key: str) -> str:
    return s3.generate_presigned_url(
        ClientMethod="get_object",
        Params={
            "Bucket": "private-media",
            "Key": key,
            "ResponseContentDisposition": 'attachment; filename="download.mp4"',
        },
        ExpiresIn=300,
    )

The official Boto3 presigned URL guide also covers presigned POSTs. Keep the region aligned with the bucket and let the SDK use SigV4; hand-built signatures create encoding and canonical-request failures that the maintained SDKs already solve.

Upload with a presigned PUT or presigned POST

PUT and POST are not interchangeable spellings. They provide different ways to constrain an upload.

Method Good fit Enforcement model Client request
Presigned PUT One object at a server-selected key Signed operation and selected headers Raw request body
Presigned POST Browser form or upload needing policy conditions Signed POST policy, fields, and conditions multipart/form-data
Multipart upload Large or resumable object Separate signed requests for each upload step and part Multiple part requests

Create a presigned PUT URL

With JavaScript v3, sign PutObjectCommand rather than GetObjectCommand:

import { PutObjectCommand } from "@aws-sdk/client-s3";

export async function createUploadUrl(key: string, contentType: string) {
  const command = new PutObjectCommand({
    Bucket: "incoming-media",
    Key: key,
    ContentType: contentType,
    ChecksumSHA256: "BASE64_ENCODED_SHA256",
  });

  return getSignedUrl(s3, command, { expiresIn: 300 });
}

The browser must send the same signed values:

await fetch(uploadUrl, {
  method: "PUT",
  headers: {
    "Content-Type": file.type,
    "x-amz-checksum-sha256": checksumBase64,
  },
  body: file,
});

If a signed Content-Type or checksum header is missing or different, expect SignatureDoesNotMatch. SigV4 supports multiple checksum algorithms for upload integrity; AWS lists the supported algorithms in its download and upload documentation.

A signed MIME header is not proof that the bytes are safe. Treat every upload as untrusted: store it under a quarantined prefix, verify the observed size and checksum, inspect the actual file type, scan when the risk calls for it, and promote it only after validation.

Use presigned POST when policy constraints matter

A POST policy can enforce a key prefix, an allowed content-type prefix, and a content-length range at S3. For example:

def create_image_post(user_id: str):
    key_prefix = f"quarantine/{user_id}/"

    return s3.generate_presigned_post(
        Bucket="incoming-media",
        Key=key_prefix + "${filename}",
        Fields={"Content-Type": "image/"},
        Conditions=[
            ["starts-with", "$key", key_prefix],
            ["starts-with", "$Content-Type", "image/"],
            ["content-length-range", 1, 25 * 1024 * 1024],
        ],
        ExpiresIn=300,
    )

The client must submit every returned field along with the file. S3's POST policy reference defines content-length-range and the condition syntax. Application-side validation still matters because MIME types can be misleading and a permitted file can contain malicious content.

Use multipart presigning for large media

A single presigned PUT cannot resume cleanly after a broken connection. For large uploads, initiate a multipart upload on the server, issue a short-lived presigned UploadPart URL for each authorized part number, collect the returned ETags, and complete the upload on the server after verifying the expected part set.

Amazon S3 recommends considering multipart upload at around 100 MB and currently permits up to 10,000 parts, with most parts between 5 MiB and 5 GiB (multipart upload limits). Always complete or abort abandoned uploads; otherwise stored parts continue to incur charges. A lifecycle rule that aborts incomplete multipart uploads is a useful cleanup backstop.

If the application may move between providers, test multipart initiation, part signing, checksums, completion, and abort behavior against each implementation. The internal S3-compatible storage guide explains why API compatibility should be verified operation by operation rather than assumed from the label.

A secure presigned upload flow validates identity, policy, file content, and final object state

Secure an S3 presigned URL workflow

Security comes from the controls around the signature, not from the length of the query string. Use this checklist before shipping.

1. Authorize before signing

Authenticate the caller, load the application resource, and verify tenant, ownership, role, plan, and state. Sign only the server-derived key and required operation. Rate-limit the presign endpoint so it cannot become an unlimited source of valid tokens.

For uploads, let the server choose a random key under a tenant-scoped quarantine prefix. Record the expected uploader, size, media class, checksum, and expiration in your database. Do not make the original filename the authorization boundary.

2. Give the signer least privilege

Use a role dedicated to presigning, not a broad administrator identity. Scope its actions, buckets, and prefixes to the workflow: for example, s3:GetObject under a download prefix or s3:PutObject under a quarantine prefix. Because the URL delegates the signer's capability, an overpowered signer produces overpowered URLs.

Prefer temporary role credentials and rotate them normally. Remember that their remaining session lifetime caps the URL lifetime. Never place the role credentials or signing code in a public client.

3. Use the shortest practical lifetime

Five to fifteen minutes is a reasonable starting range for an interactive transfer request, not a universal rule. Account for slow clients, queued uploads, multipart coordination, and retry behavior, then measure. Long-running background processes may need a refreshed URL per operation rather than one link that remains valid for days.

Administrators can enforce a maximum signature age in policy with s3:signatureAge. AWS's presigned URL guardrail guidance shows how this can deny old signatures even when their declared expiration is longer. Test clock skew and delivery delays before applying a very tight organization-wide limit.

4. Keep URLs out of accidental storage

Use HTTPS, return the token only to the authorized client, and never put it in analytics events, support tickets, source control, or application logs. Query strings can leak through browser history, screenshots, proxy logs, copied chat messages, and referrer behavior. Redact X-Amz-* parameters at every logging layer.

Avoid embedding long-lived presigned URLs in durable HTML or emails. Generate on demand or redirect from an authenticated endpoint. For responses that contain a URL, use an appropriate cache policy such as Cache-Control: private, no-store so an intermediary does not preserve the token.

5. Constrain and verify uploads

Enforce what S3 can enforce in the signed request or POST policy: key, operation, checksum, size range, and permitted form fields. Then verify what only your application can know: the actual byte format, malware status, media decodability, account quota, and whether the upload completed before its business deadline.

Do not grant read access to the quarantine prefix. Move or copy a validated object to its final key, mark the database record ready, and only then generate download access. This separates “S3 accepted bytes” from “the application trusts this asset.”

6. Treat CORS as browser plumbing, not authorization

Browser uploads need a bucket CORS rule that allows the application origin, method, and required headers. Configure the narrowest origin and method list that works. CORS controls whether browser JavaScript may make or read a cross-origin request; it does not stop a non-browser client that already possesses the signed URL.

7. Audit the complete lifecycle

Log the application event without logging the URL: user, tenant, object ID, key, method, requested lifetime, policy decision, and request correlation ID. Monitor abnormal presign rates, repeated upload failures, unexpected object sizes, uncompleted multipart sessions, and downloads from unusual contexts. Object-store access logs and CloudTrail data events can add storage-side evidence, but tie them back to the application authorization record.

For media delivery, also decide whether direct object-store transfer is the right final path. ZeroBuffer pairs S3-compatible storage at $0.01/GB per month per replica with global CDN delivery from a flat $0.0049/GB, plus origin shield and configurable cache policies. That does not replace your entitlement service or prove presigning compatibility by itself; it gives you a separate storage-and-delivery path to test when direct regional object URLs create latency, origin load, or unpredictable egress economics.

S3 presigned URL vs CDN signed URL

Both mechanisms create expiring bearer tokens, but they authorize different layers.

Requirement S3 presigned URL CDN signed URL CDN signed cookie
Direct private object download Strong fit Requires CDN and private origin setup Usually unnecessary
Direct browser upload Strong fit with PUT, POST, or multipart Usually not the upload mechanism No
Globally cached private download Not the entitlement layer for the CDN Strong fit Strong fit
One restricted file Yes Yes Possible but broader than needed
Many HLS/DASH files One URL per object becomes awkward Possible per object Often the cleanest fit
IP or time-window policy at delivery edge Bucket-policy dependent Common custom-policy capability Common custom-policy capability

An S3 presigned URL sends the client to an S3 endpoint and proves that one storage request is authorized. A CDN signed URL proves that a viewer may retrieve a resource through a distribution, where the object may be served from an edge cache while the origin remains private.

For one private invoice, either direct presigned S3 or a CDN-signed object can work; scale, latency, and cost decide. For a segmented video stream, signing every manifest, rendition playlist, segment, caption, and key URL becomes brittle. AWS recommends CloudFront signed cookies when a viewer needs access to multiple restricted files, including all files for a video, and signed URLs for individual files (CloudFront selection guidance).

The internal CDN for video guide provides the corresponding delivery checklist for manifests, segments, range requests, cache behavior, and playback telemetry.

Keep the trust boundaries explicit. The application decides entitlement. The token delegates a bounded request. The object store protects the origin. The CDN controls viewer access and caching when it is in the path. One signed URL should not be asked to perform all four jobs.

Troubleshoot S3 presigned URL errors

Most failures are deterministic once you compare the request that was signed with the request that arrived.

SignatureDoesNotMatch

Check the HTTP method, bucket region, host, encoded object key, query string, and every signed header. A common upload bug is signing Content-Type: video/mp4 and sending a different value—or omitting the header. Proxies that rewrite the host or normalize signed values can also invalidate the signature.

Capture a redacted request, not the live token. Compare SDK and server clocks, and confirm that neither client code nor a URL builder decoded and re-encoded the query parameters.

AccessDenied or an unexpectedly early expiry

Confirm that the signing principal still has permission for the exact action and resource. Then inspect the role-session expiry, explicit denies, bucket policy, access point policy, KMS key policy, and organization controls. A 24-hour expiresIn setting cannot outlive a role session with 20 minutes remaining.

For encrypted objects, the signer may also need the relevant KMS permission. For uploads, verify ownership controls and any required encryption headers that were included during signing.

The browser reports a CORS error

Verify that the browser's Origin, method, requested headers, and exposed response headers match the S3 CORS configuration. Test the same URL with curl; if that fails too, the root cause is signature or authorization, not CORS. If curl succeeds and the browser preflight fails, inspect the OPTIONS response and browser console.

The upload succeeded but the application cannot find it

Do not infer completion from the client alone. Confirm the exact key, bucket, region, size, checksum, and version ID or ETag as appropriate. Have the client notify your API, then let the backend issue HeadObject and run its validation workflow. For multipart uploads, completion is a separate signed or server-side operation; uploaded parts are not a finished object.

Frequently asked questions

What is an S3 presigned URL?

An S3 presigned URL is a bearer URL that grants temporary permission to perform one signed operation on a specific S3 resource. It lets a client upload or download directly without receiving AWS credentials or making the bucket public.

How long can an S3 presigned URL last?

With SigV4, an S3 presigned URL can declare a lifetime from one second to seven days. Its effective lifetime can be shorter because temporary signing credentials expire, permissions change, or a bucket policy limits signature age.

Can an S3 presigned URL be used more than once?

Yes. A standard presigned URL can be reused until it expires or another control invalidates it. If the workflow requires one-time behavior, track redemption in the application and prevent a second business action rather than assuming the S3 URL is single-use.

Is an S3 presigned URL safe to share?

It is safe only with the intended recipient and through a protected channel for the minimum practical time. Anyone who obtains the complete URL can use its delegated permission, so keep it out of logs, analytics, browser caches, and durable messages.

Does a presigned URL expose AWS credentials?

It exposes the access-key identifier and signing metadata, not the secret access key. The signature still delegates the signer's permitted action, so the full URL must be protected like a temporary credential.

Should I use a presigned PUT or presigned POST for uploads?

Use presigned PUT for a simple upload to one fixed key when signed headers provide enough control. Use presigned POST when S3 must enforce policy conditions such as a key prefix or content-length range; use multipart presigning when a large upload needs parallelism and resumability.

Why does my presigned URL return 403 before expiresIn ends?

The temporary credentials may have expired, the signer may have lost permission, an explicit policy may deny the request, or the actual method, region, query, or signed headers may differ. Check the credential session and policy first, then compare the received request with the signed request.

When should I use a CDN signed URL instead?

Use a CDN signed URL when private content should be cached and delivered through edge locations rather than fetched directly from the object store. For access to many related files, such as an HLS stream, signed cookies are often simpler than generating a separate signed URL for every segment.

Make the signed request the smallest part of the design

Use an S3 presigned URL when your application needs to authorize a specific direct upload or download without proxying the file. Keep the bucket private, authenticate before signing, derive the object key on the server, use a least-privilege role, choose a short realistic lifetime, and validate uploads after storage accepts them.

Then test the boundary that actually matches the workload. Use PUT or POST for constrained client uploads, multipart signing for large files, and a CDN access mechanism for private media that needs global caching or many related objects. Start with one end-to-end test that records the authorization decision, signs a five-minute request, transfers the object, validates the result, and proves the same URL fails after its intended window.

If that test points at global media rather than one-off private files, choose the storage and the delivery path together. ZeroBuffer pairs S3-compatible storage at $0.01/GB-month per replica—presigned with the same SigV4 SDK flows shown above—with flat CDN delivery from $0.0049/GB, origin shielding, instant purge, and configurable cache policies across 100+ edge locations. One vendor covers the private object store and the cached viewer path, instead of signing every segment against a regional bucket. See how storage and delivery are priced.

Topics covered

s3 presigned urlaws presigned urlsigned url

On this page

  • How an S3 presigned URL works
  • Generate an S3 presigned URL for downloads
  • Upload with a presigned PUT or presigned POST
  • Secure an S3 presigned URL workflow
  • S3 presigned URL vs CDN signed URL
  • Troubleshoot S3 presigned URL errors
  • Frequently asked questions
  • Make the signed request the smallest part of the design

Global delivery without the egress tax

100+ edge locations, 25ms average latency, free video encoding — at a flat $0.0049/GB for every region.

Start for freePricing

Keep reading

Secure Content Delivery & Access Control

Geo Blocking Video Content: How Edge Geo-Restriction Works

August 18, 202617 min
S3-Compatible Object Storage (C3)

S3 Compatible Storage: What It Means and How to Migrate Without Lock-In

June 18, 202613 min
CDN Architecture and Delivery Costs (`cdn-architecture-costs`)

S3 Egress Costs: Fees, Pricing, and How to Reduce Them

August 10, 202611 min
All articles

ZeroBuffer™

High-performance CDN, storage, streaming and optimization — built for global scale.

Product

  • Home
  • About Us
  • Features
  • Network
  • Use Cases
  • Pricing
  • Contact Us

Solutions

  • CDN for OTT
  • CDN for Gaming

Company

  • Blogs
  • Contact
  • Careers

Legal

  • Privacy Policy
  • Terms of Service
  • Acceptable Use Policy
  • Refund Policy
  • Report Abuse
© 2026 Apexnova Private Limited. All rights reserved.