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


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.
β Recommended Pattern: Keep bucket private, use pre-signed URLs
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.
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