14.6 Presigned URLs: Granting Temporary Access Without AWS Credentials
Right, let’s talk about one of the most useful Swiss Army knives in the S3 toolkit: the presigned URL. Here’s the core problem it solves: you have an object in a private bucket. You want to let someone—a user on your website, a colleague, a third-party—download it (or upload it) without giving them your precious, all-powerful AWS credentials. You also don’t want to make the bucket public and unleash chaos upon the world.
A presigned URL is your answer. It’s a time-limited, feature-limited key you generate with your own credentials. You hand this key to someone, and for a defined period, they can perform a single operation (like GET or PUT) on that single object. After the expiration time, the URL becomes useless. It’s elegant, secure, and brilliantly simple.
The Nuts and Bolts of Generating a GET URL
Let’s generate a URL so a user can download company-report-q4.pdf. We’ll use the AWS SDK for Python (Boto3). The logic is nearly identical in other languages.
import boto3
from datetime import datetime, timedelta
s3_client = boto3.client('s3')
# Generate the URL. Expires in 7 days because this report is big and the CFO is on vacation.
url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': 'my-super-secret-bucket',
'Key': 'financial/company-report-q4.pdf'
},
ExpiresIn=604800 # Seconds. 7 days = 60*60*24*7
)
print(f"Here's your one-time link: {url}")
That’s it. The URL will look like a horrifying mess of query parameters, the most important being X-Amz-Expires, X-Amz-Signature, and X-Amz-Credential. This is the magic sauce—cryptographic proof that you authorized this request, baked right into the URL itself. The recipient’s browser just sends this URL; AWS validates the signature and expiration time and, if everything checks out, serves the object.
Wait, You Can Upload with These Too?
Absolutely. This is the killer feature many miss. You can generate a URL that allows someone to PUT an object directly into your bucket without them having an AWS account. Think of user-generated content, file uploads from a web app, or letting a client send you a large video file.
# Generate a URL for uploading a new object
upload_url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params={
'Bucket': 'my-user-uploads-bucket',
'Key': 'incoming/vacation-photo.jpg',
'ContentType': 'image/jpeg' # Highly recommended to enforce this!
},
ExpiresIn=3600 # This link is only good for 1 hour
)
print(f"Upload your file here: {upload_url}")
You’d then hand this URL to your frontend JavaScript code, which would use it to PUT the file directly to S3, offloading that traffic from your web server. It’s a beautiful thing.
The Devilish Details and “Gotchas”
This is where I earn my keep. Presigned URLs are simple, but the edges are sharp.
1. Expiration is Non-Negotiable: Once that clock runs out, the URL is dead. There is no revoking a single presigned URL before its expiration, short of rotating your entire IAM credential set (which is a nuclear option). Plan your expiration times wisely. For a download, make it long enough to be useful but short enough to minimize risk. For an upload, an hour or two is usually plenty.
2. The Client is on the Hook for Performance: When you give someone a presigned GET URL, the data transfer happens directly between their client and AWS. It doesn’t flow through your servers. This is great for your bandwidth bill but terrible for your ability to monitor progress or speed. If their connection is bad, their download will be slow. You’re just the ticket booth, not the projector.
3. You Can’t “Require” a Save-As Dialog: A very common question: “How do I force a download instead of having the file open in the browser?” You do this by setting the Content-Disposition header on the object itself in S3. You have to plan ahead. You can’t dynamically set this in the presigned URL generation for a GET operation. The workaround is to upload your objects with the correct metadata: s3_client.put_object(..., ContentDisposition='attachment'). Alternatively, for a GET request, you can override the response headers using the Params like so, but the user’s browser may still choose to ignore it.
url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': 'my-bucket',
'Key': 'my-file.pdf',
'ResponseContentDisposition': 'attachment; filename="my-cool-report.pdf"'
},
ExpiresIn=300
)
4. Permissions Still Apply: This is the most important rule. Your IAM user/role must have permission to perform the operation you’re presigning. If your credentials don’t have s3:GetObject permission for that file, you cannot generate a valid presigned URL for it. The authorization happens at the moment of generation, not use.
Best Practices from the Trenches
- Use Short Expirations for Uploads: An upload URL is a potential attack vector. Keep its lifetime short—minutes or hours, not days.
- Be Specific with Upload Parameters: When generating a
PUTURL, specifyContentType(and evenContentLengthif you can) in theParams. This prevents a user from uploading a 100GB file when you expected a 10MB JPEG or uploading an executable as an image. - Consider CloudFront for Advanced Use Cases: If you find yourself needing to revoke access centrally or want better metrics, look into using CloudFront Signed URLs/Cookies instead of S3 presigned URLs. It’s a more complex setup but offers more control.
- HTTPS, Always: Generate your URLs with
https. There’s no reason to ever use an unencryptedhttppresigned URL.
Presigned URLs are a masterpiece of pragmatic security. They let you open a tiny, well-defined window into your private storage without tearing down the walls. Use them with confidence, but respect their sharp edges.