πŸ” Secure S3 Object Access with Pre-Signed URLs and Fine-Grained Bucket Policies

Zahoor FarooqZahoor Farooq
3 min read

Overview

In production-grade systems, granting temporary and controlled access to S3 objects is a common requirement, whether for CI/CD pipelines, external integrations, or user-facing downloads. AWS pre-signed URLs are the recommended method for achieving this without compromising bucket security.

This post explores the mechanics, security implications, implementation details, and how pre-signed URLs interact with S3 IAM policies and bucket configurations.


πŸ“Œ Why Pre-Signed URLs?

By default, private S3 objects are accessible only by IAM principals with explicit s3:GetObject permission. In scenarios like:

  • Temporary file downloads (e.g., invoices, reports)

  • Secure external sharing (without assuming roles)

  • Authenticated uploads from frontend apps

…pre-signed URLs provide a scalable, auditable, and permission-safe alternative to exposing your buckets or manually rotating access keys.


βš™οΈ How Pre-Signed URLs Work (Under the Hood)

A pre-signed URL is essentially a cryptographic signature generated using:

  • AWS credentials (Access Key + Secret Key)

  • Canonical request (HTTP method, bucket, object key, etc.)

  • Expiry timestamp

  • AWS Signature Version 4

The URL includes signed query parameters:

X-Amz-Algorithm
X-Amz-Credential
X-Amz-Date
X-Amz-Expires
X-Amz-SignedHeaders
X-Amz-Signature

πŸ“Œ Note: Anyone with this URL can access the object as if they were the signer, until expiration.


πŸ” IAM Requirements for Signer Identity

To generate a pre-signed URL, the signer (IAM user or role) must have s3:GetObject (or s3:PutObject) on the object.

Example IAM policy:

{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::secure-assets-prod/*"
}

πŸ› οΈ Generating Pre-Signed URLs

πŸ”§ AWS CLI

aws s3 presign s3://secure-assets-prod/files/report.csv --expires-in 900

πŸ“Œ Outputs a URL valid for 15 minutes.


🐍 Python (Boto3)

import boto3

s3 = boto3.client('s3')
url = s3.generate_presigned_url('get_object',
    Params={'Bucket': 'secure-assets-prod', 'Key': 'files/report.csv'},
    ExpiresIn=900)
print(url)

For uploads:

url = s3.generate_presigned_url('put_object',
    Params={'Bucket': 'secure-assets-prod', 'Key': 'uploads/newfile.pdf'},
    ExpiresIn=300)

🧰 Bucket Policy vs Pre-Signed URLs

❌ Anti-pattern: Making the bucket public

{
  "Effect": "Allow",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-bucket/*"
}

This should not be used in production unless you're hosting static sites or intentionally allowing unrestricted access.


Ensure your bucket has Block Public Access enabled:

S3 β†’ Bucket β†’ Permissions β†’ Block public access (enabled for all)

πŸ“Œ Integration Use Cases

βœ… Checklist: Production-Ready Usage

  • Bucket public access blocked

  • Use IAM roles with least privilege

  • Short expiry windows (5-15 minutes)

  • Audit S3 access with CloudTrail

  • Rotate the credentials used for signing

  • Use HTTPS always


πŸ“¦ Wrap-Up

Pre-signed URLs allow secure, scoped, and time-bound access to S3 objects β€” a critical need in modern backend systems. Avoid public buckets wherever possible and integrate presigned URLs to maintain security, observability, and scalability.

If you're building a file-sharing feature, CDN-backed access layer, or dynamic file downloads in apps β€” presigned URLs are the way to go.

0
Subscribe to my newsletter

Read articles from Zahoor Farooq directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Zahoor Farooq
Zahoor Farooq

Code enthusiastic fr