Ravindra BagaleCourses & study guides Track your progress

Guides

S3 Presigned URL: Let Users Upload and Download Files Securely (CLI, Python, Node.js, Java)

An S3 presigned URL is a time-limited link that lets someone download (GET) or upload (PUT) one specific object in a private bucket, without AWS credentials and without making the bucket public. Create a download link with aws s3 presign s3://bucket/file --expires-in 600, and create upload links in your backend with the SDK (generate_presigned_url in Python, getSignedUrl in Node.js). Keep the expiry short and the bucket's Block Public Access on.

Quick answer

# download link valid for 10 minutes
aws s3 presign s3://rb-invoices-2026/inv-1001.pdf --expires-in 600
# upload link (Python boto3), valid for 5 minutes
url = s3.generate_presigned_url("put_object",
    Params={"Bucket": "rb-uploads-2026", "Key": f"users/{user_id}/photo.jpg",
            "ContentType": "image/jpeg"}, ExpiresIn=300)

A live example first

A training institute stores student certificates (PDFs) in a private bucket, rb-certs-2026. A student should be able to download only their own certificate.

  1. Try the plain object URL in a browser:
https://rb-certs-2026.s3.ap-south-1.amazonaws.com/2026/RB-1042.pdf
→ AccessDenied

Good. The bucket is private.

  1. The backend creates a 10-minute link for this student only:
aws s3 presign s3://rb-certs-2026/2026/RB-1042.pdf --expires-in 600 --region ap-south-1
https://rb-certs-2026.s3.ap-south-1.amazonaws.com/2026/RB-1042.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA.../20261016/ap-south-1/s3/aws4_request&X-Amz-Date=20261016T190500Z
&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=8f2c...
  1. The link opens the PDF. After 10 minutes the same link shows:
<Code>AccessDenied</Code><Message>Request has expired</Message>

Friends, the bucket never became public. The link carried a small, temporary permission for one file.

How does a presigned URL work?

S3 presigned URL flow The backend signs a short-lived URL, the browser uploads or downloads directly with S3, and the bucket stays private. S3 presigned URL flow Userasks backendBackendsigns URLURLexpires 10 minS3private bucketDonedirect transfer you The bucket stays private. The link is a short-lived permission for one object

Your backend signs a short-lived URL for one object, and the user talks to S3 directly while the bucket stays private.

Your backend has AWS credentials (ideally an IAM role). It signs the request details (method, bucket, key, expiry, and some headers) with those credentials, and puts the signature in the URL. When the user calls S3 with that URL, S3 checks the signature and the expiry, and checks that the signer is allowed to do that action. If the signer's role loses permission, the link stops working too.

How do I use presigned URLs step by step?

  1. Keep Block Public Access on for the bucket.
  2. Give your backend role only s3:GetObject / s3:PutObject on the needed prefix.
  3. When a logged-in user asks, check they own that file.
  4. Generate a URL for exactly that key, with a short expiry (5–15 minutes).
  5. For browser uploads, add a CORS rule to the bucket.
  6. After upload, validate the file (size, type) before using it.

Examples in each language

Python (boto3)

import boto3
s3 = boto3.client("s3", region_name="ap-south-1")
url = s3.generate_presigned_url("get_object",
        Params={"Bucket": "rb-certs-2026", "Key": "2026/RB-1042.pdf"}, ExpiresIn=600)

Node.js (AWS SDK v3)

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "ap-south-1" });
const url = await getSignedUrl(s3,
  new PutObjectCommand({ Bucket: "rb-uploads-2026", Key: `users/${userId}/photo.jpg`, ContentType: "image/jpeg" }),
  { expiresIn: 300 });

Java (AWS SDK v2)

S3Presigner presigner = S3Presigner.create();
GetObjectPresignRequest req = GetObjectPresignRequest.builder()
    .signatureDuration(Duration.ofMinutes(10))
    .getObjectRequest(b -> b.bucket("rb-certs-2026").key("2026/RB-1042.pdf"))
    .build();
String url = presigner.presignGetObject(req).url().toString();

.NET

var url = s3Client.GetPreSignedURL(new GetPreSignedUrlRequest {
    BucketName = "rb-certs-2026", Key = "2026/RB-1042.pdf",
    Expires = DateTime.UtcNow.AddMinutes(10), Verb = HttpVerb.GET });

Browser upload with the URL

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

The Content-Type must match what the backend signed, or you get SignatureDoesNotMatch.

CORS rule for browser uploads

[
  {
    "AllowedOrigins": ["https://ravindrabagale.com"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedHeaders": ["Content-Type"],
    "MaxAgeSeconds": 3000
  }
]

Add it in S3 → bucket → Permissions → Cross-origin resource sharing (CORS).

Limits and errors

Topic Detail
Max expiry (IAM user keys, SigV4) 7 days
Max expiry (role / temporary credentials) Until those credentials expire
Single PUT size Up to 5 GB; larger files need multipart (presign each part)
Request has expired The link is too old; make a new one
SignatureDoesNotMatch Different headers, key or region than signed
AccessDenied The signer lacks permission, or a bucket policy denies it

The same idea on other clouds

Google Cloud Storage has signed URLs, and Azure Blob Storage has SAS tokens (shared access signatures). They all give short-lived, limited access without making storage public.

Many large data leaks have come from S3 buckets left public. For example, in 2017 security researchers at UpGuard reported that a public bucket used by a Verizon partner exposed records of millions of customers. Because of leaks like this, AWS turned on Block Public Access and disabled ACLs by default for all new buckets from April 2023. Presigned URLs are the safe way to share files while keeping those protections on.

Mistakes to avoid

  1. Making the bucket public "just for downloads". Use presigned URLs.
  2. Week-long links for private documents. Use minutes.
  3. Letting users choose the full key. Build it on the server (users/<id>/...), or one user can overwrite another's file.

Summary table

Task How
Download link (CLI) aws s3 presign s3://​b/​k --​expires-​in 600
Upload link SDK put_object / PutObjectCommand presign
Browser upload fetch(url, {method:"PUT"}) + bucket CORS
Keep safe Private bucket, short expiry, server-built keys
Errors expired, SignatureDoesNotMatch, AccessDenied

Quick quiz: check what you learned

  1. What does a presigned URL let someone do?

    1. One action on one object
    2. List every bucket you own
    3. Change your IAM policies
    4. Read all objects forever
    Show answer

    A. It allows only the signed method on the signed key until expiry.

  2. What is the longest expiry with IAM user keys (SigV4)?

    1. 7 days
    2. 1 hour
    3. 30 days
    4. 1 year
    Show answer

    A. SigV4 presigned URLs can last up to 7 days.

  3. A browser PUT upload fails with a CORS error. Fix?

    1. Add a bucket CORS rule
    2. Make the bucket public
    3. Raise the Lambda timeout
    4. Turn off Block Public Access
    Show answer

    A. The bucket must allow your origin and the PUT method.

  4. Why keep presigned URLs short-lived?

    1. Anyone with the link can use it
    2. S3 charges per minute of URL
    3. Long URLs break the bucket
    4. The SDK rejects long expiry
    Show answer

    A. A leaked link works for everyone until it expires.

Learn it properly

These pages go deeper, with labs:

Frequently asked questions

What is an S3 presigned URL?

A normal S3 link plus a temporary signature made with your AWS credentials. Anyone with the link can do that one action (GET or PUT) on that one object until it expires.

How long can a presigned URL last?

Up to 7 days when signed with an IAM user's long-term keys (Signature Version 4). When signed with temporary role credentials, it stops working when those credentials expire, which is often within hours.

Do I need to make the bucket public?

No. That is the point. The bucket stays private, and only people with a valid link can access the one object.

Why does my browser upload fail with a CORS error?

The bucket needs a CORS rule allowing your website origin, the PUT method and the headers you send.

Why do I get SignatureDoesNotMatch?

The request does not match what was signed, for example a different Content-Type header, region or object key.

Can someone reuse a presigned upload URL?

Yes, until it expires. Keep expiry short, use a unique key per upload, and check the file after upload.