S3 Presigned URLs

Generate and use S3 presigned URLs — AWS Cloud & DevOps with Python.

Focus: generate and use s3 presigned urls

Sponsored

You've built an application that stores private files in Amazon S3, and now you need to securely share those files with users — without making your bucket public or forcing every download through your own servers. This is exactly the problem that S3 presigned URLs solve: they give you time-limited, permission-scoped access to private S3 objects, and you can generate them in a few lines of Python using the official boto3 SDK. In this lesson, you'll learn how to generate and use S3 presigned URLs, why they're a cornerstone of secure cloud architecture, and how to avoid the common pitfalls that trip up even experienced developers.

The problem this lesson solves

Imagine you run a document management platform where users upload contracts, invoices, and other sensitive files. You store them in a private S3 bucket with server-side encryption. When a user needs to download one of their files, you have a few options — and most of them are bad:

  • Make the bucket public: Now anyone on the internet can list or access objects if they know the key. That's a data breach waiting to happen.
  • Proxy the download through your own server: Your EC2 or Lambda instance downloads the object from S3 and streams it back to the user. This works, but it burns your bandwidth, doubles your latency, and makes your app the bottleneck.
  • Use pre-authenticated access: Issue a URL that carries a temporary signature, valid for a few minutes or hours, that lets the user reach the object directly in S3 — bypassing your server entirely.

The third option is a presigned URL, and it's the AWS-native solution to this exact problem. Without presigned URLs, you'd either compromise security or pay a performance and cost penalty on every file download.

But the need goes beyond downloads. Presigned URLs also power secure uploads — letting users upload files directly to S3 without exposing your bucket's write permissions or pushing large binaries through your application servers. Once you master presigned URLs, you can design file-sharing flows that are both fast and secure.

Core concept / mental model

A presigned URL is an S3 object URL that includes a cryptographic signature in its query parameters. That signature is derived from your AWS credentials (via the AWS Signature Version 4 process) and encodes three things:

  • Who — the IAM principal (user or role) whose permissions are in effect when the URL is used.
  • What — the specific S3 operation allowed (e.g., GET or PUT) on a specific object.
  • How long — the expiration timestamp, after which the signature is invalid.

Think of it like a valet parking ticket. You hand the ticket to a guest, and it grants them the right to retrieve one specific car (your S3 object) from the garage (your bucket) — but only until the ticket expires. The garage verifies the signature on the ticket, not your identity, and won't honor it after the time window closes.

Another analogy: a presigned URL is a pre-paid envelope with a stamp and sender address pre-filled. You can give it to someone who then sends mail (upload) or receives mail (download) on your behalf, but only for the specified action and timeframe.

Here's how the pieces fit together in AWS:

  • S3 bucket — container for objects.
  • Object key — the unique path to the object in the bucket (e.g., uploads/report-2025.pdf).
  • Signing credentials — the AWS access key ID and secret access key (or session token for temporary credentials) used to compute the signature.
  • Signature Version 4 — the cryptographic algorithm that hashes the request parameters with your secret key.
  • Expiration — the number of seconds the URL remains valid (default 3600 seconds, i.e., 1 hour, when using boto3).

A presigned URL is not a token, an API key, or a session — it's a self-contained, time-limited grant. Once generated, it can be used by anyone who has the URL, until it expires. That's both its power and its danger.

How it works step by step

When you generate a presigned URL with boto3, the SDK performs the following steps under the hood:

  1. You identify the operation — typically get_object (download) or put_object (upload). You also specify the bucket name, object key, and optionally the expiration time.
  2. The SDK constructs an HTTP request — method (GET or PUT), URL (https://bucket.s3.amazonaws.com/key), and any parameters (e.g., Content-Type for uploads).
  3. The SDK signs the request — it uses your configured AWS credentials to compute an AWS Signature Version 4 signature, incorporating the current timestamp, the region, and the service (s3).
  4. The SDK appends query parameters to the URL: X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-Signature, and sometimes X-Amz-Security-Token (if you're using temporary credentials).
  5. You share the URL — the recipient can use it with any HTTP client (browser, curl, requests) to perform the signed operation on the object.

The signature is tied to the exact operation, object, and expiration. You cannot change the HTTP method, the object key, or the expiration after generation without invalidating the signature.

For downloads (GET), the flow looks like this:

User clicks link → Browser sends GET to S3 → S3 verifies signature & expiration → S3 returns object (or error)

For uploads (PUT), the browser sends a PUT request with the file content; the presigned URL allows that write because the signature was computed with your credentials that have s3:PutObject permission.

Hands-on walkthrough

Prerequisites

  • Python 3.8+ with boto3 installed: pip install boto3
  • AWS credentials configured via ~/.aws/credentials or environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optionally AWS_SESSION_TOKEN)
  • An S3 bucket that you can create or already have. Let's call it my-private-bucket (replace with a globally unique name).

Generate a download presigned URL

Here's a complete script that generates a presigned URL for a private object:

import boto3
from botocore.exceptions import ClientError

BUCKET = "my-private-bucket"
OBJECT_KEY = "reports/quarterly.pdf"
EXPIRATION = 300  # 5 minutes

def create_presigned_url(bucket_name, object_key, expiration=3600):
    """Generate a presigned URL for downloading an S3 object."""
    s3_client = boto3.client("s3")
    try:
        url = s3_client.generate_presigned_url(
            ClientMethod="get_object",
            Params={"Bucket": bucket_name, "Key": object_key},
            ExpiresIn=expiration
        )
    except ClientError as e:
        print(f"Error generating presigned URL: {e}")
        return None
    return url

if __name__ == "__main__":
    url = create_presigned_url(BUCKET, OBJECT_KEY, EXPIRATION)
    if url:
        print("Presigned URL:")
        print(url)

Expected output (format varies):

Presigned URL:
https://my-private-bucket.s3.amazonaws.com/reports/quarterly.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...%2F20250101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250101T120000Z&X-Amz-Expires=300&X-Amz-Signature=abcdef...

Now you can share this URL with a trusted user. They can download the file with a browser or curl:

curl -O "<the presigned URL>"

Generate an upload presigned URL

To allow a user to upload a file directly to S3, generate a URL for put_object:

import boto3

def create_presigned_upload_url(bucket_name, object_key, expiration=600):
    """Generate a presigned URL for uploading to S3."""
    s3_client = boto3.client("s3")
    return s3_client.generate_presigned_url(
        ClientMethod="put_object",
        Params={"Bucket": bucket_name, "Key": object_key},
        ExpiresIn=expiration
    )

if __name__ == "__main__":
    url = create_presigned_upload_url("my-private-bucket", "uploads/user1/profile.png")
    print(url)
    # User can then upload with curl:
    # curl -X PUT --upload-file profile.png "<url>"

The recipient uploads by sending a PUT request with the file content to the presigned URL.

Verify the URL works (and expires)

A quick test with requests:

import requests
from create_download_url import create_presigned_url

url = create_presigned_url("my-private-bucket", "reports/quarterly.pdf", 60)
response = requests.get(url)
print(response.status_code)  # 200 if the object exists and the URL is valid

Compare options / when to choose what

Presigned URLs aren't the only way to grant access to S3 objects. Here's a comparison:

Approach Security Performance Use case
Presigned URLs High (scoped, time-limited) Excellent (direct S3 access) Temporary sharing, direct uploads
Public bucket (anyone read) None Excellent Public assets (websites, images) — not for private data
S3 bucket policy with IP/Referer restriction Medium Excellent Restricting access to a specific IP range or referrer — hard to manage for many users
Proxy via your own backend High (full control) Poor (extra hop) When you need post-download processing or logging
AWS Cognito + fine-grained IAM Very high Good (requires auth flow) User-specific access within an authenticated app

When to choose presigned URLs: - You need to share a private object with someone for a short period (e.g., a reset link or a shared report). - You want users to upload files directly to S3 without giving them AWS credentials. - You want to offload traffic from your application server for large downloads.

When to avoid them: - The object is permanently public (use a public bucket or a CloudFront distribution). - You need to count downloads or run custom logic before serving (use a proxy). - You require per-user identity and auditing (use Cognito or IAM).

Troubleshooting & edge cases

1. ClientError: The specified key does not exist

If you get a 404 when using the presigned URL, the object doesn't exist at the path you specified, or the bucket is in a different region than your client is configured for. Double-check the Key, the bucket name, and your AWS region (set region_name in boto3.client).

2. SignatureDoesNotMatch error

This error means the signature is invalid. Common causes: - Time skew: The recipient's system clock is off by more than the allowed tolerance (usually 15 minutes). Fix the clock. - Region mismatch: The URL was generated for one region but the bucket is in another. Ensure you're using the correct region in your boto3 client. - Temporary credentials expired: If you used an IAM role, the session token may have expired; regenerate the URL with fresh credentials.

3. Access denied even though the URL is valid

Remember that presigned URLs inherit the permissions of the signer. If your IAM user/role doesn't have s3:GetObject permission on that object, the URL will fail with a 403, regardless of the signature. Make sure your IAM policy allows the operation for the bucket/object (e.g., arn:aws:s3:::my-private-bucket/*).

4. URL expires too soon (or too late)

Set ExpiresIn to a sensible value. For downloads, 5–15 minutes is common; for uploads, you might allow up to 1 hour for large files. Remember, the maximum is 7 days (604800 seconds).

5. Character encoding issues in the object key

If your object key contains spaces or special characters, the URL may look odd. That's normal — the SDK URL-encodes the key. Don't modify the URL before sharing it.

6. Using presigned URLs with SSE-KMS

If your bucket uses SSE-KMS encryption, the presigned URL will fail unless you also sign with the necessary kms:GenerateDataKey permission, and the credentials used to generate the URL must have access to the KMS key. Alternatively, use SSE-S3.

What you learned & what's next

You now understand how generate and use s3 presigned urls works: you can create time-limited URLs that allow secure downloads and uploads to private S3 objects, using boto3's generate_presigned_url method. You learned the importance of IAM permissions, expiration, and the underlying signature mechanism, and you practiced both download and upload scenarios.

You can now explain the core idea behind presigned URLs and complete a practical exercise — meeting the learning objectives of this lesson.

What's next: In the next lesson, you'll explore S3 Bucket Policies and Access Control — how to enforce fine-grained permissions across your buckets, using IAM and bucket policies to complement the temporary access that presigned URLs provide. Building on this skill, you'll be able to design secure, scalable file-sharing systems.

Pro tip: Presigned URLs are a cornerstone of serverless upload/download workflows. Practice by building a small web app (Flask or FastAPI) that generates a presigned URL for each user request, then have the browser use it directly to upload a file without ever touching your backend. That's the pattern used in real production systems.

Practice recap

Generate a presigned URL for a sample object and then test it with curl before it expires. Then, write a script that creates an upload presigned URL and upload a small file to your bucket. Finally, try altering the expiration to a very short time (5 seconds) and confirm that the URL fails after it expires. This will cement your understanding of expiration and signature validity.

Common mistakes

  • Leaving the default expiration too long (e.g., 7 days) for a download link meant for a quick access.
  • Sharing a presigned URL on a public channel (like a tweet or public Slack) — anyone with the URL can use it until it expires.
  • Generating a URL with credentials that don't have the required S3 permission (e.g., no s3:GetObject), leading to a 403.
  • Ignoring the clock: if the user's system clock is skewed, the signature may not validate.
  • Trying to reuse a presigned URL after it expires instead of generating a new one.

Variations

  1. Use generate_presigned_post for browser-based uploads with HTML forms, which allows larger files and multipart behavior.
  2. Leverage AWS SDKs in other languages (e.g., JavaScript) to generate presigned URLs on the frontend with temporary credentials.
  3. Pre-sign a CloudFront URL instead of a raw S3 URL when you want CDN distribution and custom domain names.

Real-world use cases

  • Securely share a sensitive report with a client for a limited time without making the bucket public.
  • Allow users to upload profile pictures directly to S3 from a mobile app without exposing AWS credentials.
  • Generate a temporary download link for a paid e-book that expires after the purchase session ends.

Key takeaways

  • A presigned URL grants temporary, scoped access to a specific S3 object — no need to expose your bucket.
  • Use boto3's generate_presigned_url with ClientMethod='get_object' or 'put_object' and an ExpiresIn value.
  • The URL works only if the signer has the necessary IAM permissions for the operation on that object.
  • Presigned URLs are time-limited (max 7 days) and are not meant for sharing publicly — treat them like passwords.
  • Choose presigned URLs for direct S3 access; use proxies or IAM identity-based policies when you need custom logic or user identity.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.