Over the past months, I’ve been exploring ways to securely handle file uploads and downloads using Amazon S3. I’ve been experimenting with presigned URLs, testing expiration settings, validating file types, and seeing how clients can interact directly with S3 without exposing AWS credentials.
While working with AWS and FastAPI, I wanted a simple yet secure way for users to upload and download files directly from S3. That curiosity led me to build a project called s3-presigned-url-api
, a FastAPI application that generates temporary S3 presigned URLs for secure file management.
In this article, I’ll walk you through how presigned URLs work, how I implemented them in FastAPI, and what security practices you should follow when using them in production.
What Are Presigned URLs
A presigned URL is a temporary signed link that gives limited access to an S3 object. You can use it for uploads, downloads, or deletions without needing to expose AWS access keys.
Here’s how the flow works:
Client → FastAPI → AWS S3 (presigned URL) → Client → S3 (upload/download)
Your backend (FastAPI) requests a presigned URL from AWS, signs it with your credentials, and returns it to the client. The client can then upload or download directly from S3 for a limited time, usually a few minutes.
Setting Up the Project
Requirements
- Python 3.9+
- FastAPI
- boto3
- uvicorn
- python-dotenv
Install dependencies:
pip install fastapi boto3 uvicorn python-dotenv
Environment Variables
Create a .env file in your project directory:
AWS_ACCESS_KEY_ID=your_aws_access_key
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
AWS_REGION=us-east-1
S3_BUCKET_NAME=your_bucket_name
PRESIGNED_URL_EXPIRATION=600
FastAPI Code Example
s3_service.py
This module handles presigned URL generation using boto3.
`import boto3
from botocore.exceptions import ClientError
from dotenv import load_dotenv
import os
load_dotenv()
s3_client = boto3.client("s3", region_name=os.getenv("AWS_REGION"))
def create_presigned_url(bucket_name, object_name, expiration=600, method="put_object"):
try:
response = s3_client.generate_presigned_url(
ClientMethod=method,
Params={"Bucket": bucket_name, "Key": object_name},
ExpiresIn=expiration
)
except ClientError as e:
print(e)
return None
return response`
main.py
The FastAPI app exposes endpoints for upload and download URLs.
`from fastapi import FastAPI, Query
from s3_service import create_presigned_url
import os
app = FastAPI()
@app.get("/generate-upload-url")
def generate_upload_url(filename: str = Query(..., description="Name of the file to upload")):
url = create_presigned_url(os.getenv("S3_BUCKET_NAME"), filename, method="put_object")
return {"upload_url": url}
@app.get("/generate-download-url")
def generate_download_url(filename: str = Query(..., description="Name of the file to download")):
url = create_presigned_url(os.getenv("S3_BUCKET_NAME"), filename, method="get_object")
return {"download_url": url}`
Run the app:
uvicorn main:app --reload
You can now visit:
/generate-upload-url?filename=myfile.png
/generate-download-url?filename=myfile.png
to generate presigned links for uploads and downloads.
How the Upload Works
1.The client requests a presigned upload URL from your FastAPI endpoint.
2.FastAPI calls S3 through boto3 to generate a signed URL that expires in 10 minutes.
3.The client uploads the file directly to S3 using that URL.
Example upload with curl:
curl --upload-file ./test.png "$(curl -s 'http://localhost:8000/generate-upload-url?filename=test.png' | jq -r .upload_url)"
Security Considerations
Presigned URLs are powerful but must be used carefully:
- Use short expiration times, around 5 to 10 minutes
- Validate file types before allowing uploads
- Apply least privilege to IAM roles and users
- Enable server-side encryption for all uploaded files
- Log and monitor presigned URL requests for auditing
Infrastructure and Deployment
-This project includes support for:
-Docker for containerization
-Terraform for provisioning AWS resources
Kubernetes manifests for orchestration if you deploy to EKS or similar
Example Docker command:
docker build -t s3-presigned-url-api .
docker run -p 8000:8000 s3-presigned-url-api
Future Improvements
Here’s what I plan to add next:
- JWT authentication to restrict who can generate URLs
- Logging and metrics for observability
- Automatic file cleanup with S3 lifecycle policies
- Rate limiting to prevent abuse
Conclusion
Working with S3 presigned URLs has been a great learning experience. It’s a simple and secure way to handle file uploads and downloads in a cloud-native architecture. FastAPI makes it easy to expose these capabilities with clean and lightweight endpoints.
You can check out the complete source code on GitHub:
https://github.com/Copubah/s3-presigned-url-api
Top comments (0)