S3 Presigned URLs: Grant Temporary, Secure File Access in 60 Seconds
You have private files locked in an S3 bucket, but you need to give a specific user temporary download access — without exposing your AWS credentials, making the bucket public, or building a proxy download endpoint. This is exactly the problem S3 Presigned URLs solve.
TL;DR
| Concern | Answer |
|---|---|
| What is it? | A time-limited, cryptographically signed URL granting access to a private S3 object |
| Who signs it? | Your backend server using IAM credentials (never the client) |
| Expiration range | 1 second to 7 days (604800 seconds max for SigV4) |
| Required IAM permission | s3:GetObject on the target bucket/key |
| Core Fix | Use generate_presigned_url from boto3 with ExpiresIn=3600 |
How Presigned URLs Work: The Data Flow
The signing happens entirely on your server. The URL itself encodes the expiration, the target object, and a cryptographic signature derived from your IAM credentials. S3 validates this signature on every request — no session, no cookie, no backend proxy needed.
The Analogy: A Valet Parking Ticket
Think of a presigned URL like a valet parking ticket. The hotel (your backend) issues a ticket stamped with an expiry time. The valet (S3) accepts the ticket and hands over the car (file) without needing to call the hotel again — but only if the ticket is still valid and hasn't been tampered with. Once it expires, it's worthless. The guest never needs a key to the entire garage.
IAM: Minimum Required Permissions
The IAM role or user your backend uses to generate the URL must have s3:GetObject on the specific bucket and key prefix. Nothing more.
🔽 [Click to expand] IAM Policy: Least-Privilege for Presigned URL Generation
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPresignedUrlGeneration",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-private-bucket/uploads/*"
}
]
}
Critical note: The bucket policy must NOT block public access overrides for the object itself — the presigned URL bypasses bucket-level public access blocks only if the signing identity has explicit s3:GetObject permission. If you use S3 Block Public Access at the account level, presigned URLs still work because they authenticate via IAM, not anonymous access.
Implementation: Generate a Presigned URL (Python / boto3)
This is the canonical pattern. Run this on your backend — Lambda, EC2, ECS, anywhere your IAM role is attached.
💻 [Click to expand] Python (boto3): Generate Presigned URL with 1-Hour Expiry
import boto3
from botocore.exceptions import ClientError
def generate_presigned_download_url(
bucket_name: str,
object_key: str,
expiration_seconds: int = 3600 # 1 hour
) -> str | None:
"""
Generate a presigned URL to download a private S3 object.
:param bucket_name: Name of the S3 bucket
:param object_key: Full S3 key (e.g., 'uploads/reports/q1.pdf')
:param expiration_seconds: URL validity window in seconds (default: 3600)
:return: Presigned URL string, or None on failure
"""
s3_client = boto3.client(
's3',
region_name='us-east-1' # Must match bucket region for SigV4
)
try:
url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': bucket_name,
'Key': object_key,
},
ExpiresIn=expiration_seconds,
HttpMethod='GET'
)
return url
except ClientError as e:
print(f"[ERROR] Failed to generate presigned URL: {e}")
return None
# --- Usage ---
if __name__ == '__main__':
presigned_url = generate_presigned_download_url(
bucket_name='your-private-bucket',
object_key='uploads/reports/q1.pdf'
)
if presigned_url:
print(f"Share this URL (valid for 1 hour):\n{presigned_url}")
What the Signed URL Actually Contains
Decoding a presigned URL reveals the embedded parameters that S3 uses to validate every incoming request:
https://your-private-bucket.s3.amazonaws.com/uploads/reports/q1.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20240115%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20240115T120000Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<computed_hmac_sha256>
- X-Amz-Expires: Seconds from
X-Amz-Dateuntil the URL is invalid - X-Amz-Credential: Encodes the signing identity, date, region, and service
- X-Amz-Signature: HMAC-SHA256 of the canonical request — any tampering invalidates it
Node.js Implementation (AWS SDK v3)
💻 [Click to expand] Node.js (AWS SDK v3): Presigned URL with 1-Hour Expiry
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client({ region: 'us-east-1' });
async function generatePresignedDownloadUrl(
bucketName: string,
objectKey: string,
expiresInSeconds: number = 3600
): Promise<string> {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: objectKey,
});
const signedUrl = await getSignedUrl(s3Client, command, {
expiresIn: expiresInSeconds,
});
return signedUrl;
}
// Usage
const url = await generatePresignedDownloadUrl(
'your-private-bucket',
'uploads/reports/q1.pdf'
);
console.log('Presigned URL:', url);
Common Pitfalls & How to Avoid Them
| Pitfall | Root Cause | Fix |
|---|---|---|
SignatureDoesNotMatch | Clock skew between your server and AWS (>5 min) | Sync server time via NTP; use EC2/Lambda (auto-synced) |
| URL expires immediately | Generating with temporary STS credentials that expire before the URL | Ensure STS token TTL > URL expiration window |
AccessDenied on valid URL | Signing identity lacks s3:GetObject | Attach the least-privilege IAM policy shown above |
| URL works in browser but fails in curl | Special characters in object key not URL-encoded | URL-encode the key before passing to generate_presigned_url |
| Max expiry exceeded | Requesting >604800s (7 days) with SigV4 | Cap ExpiresIn at 604800; redesign flow if longer access needed |
Architecture: Where Presigned URL Generation Lives
Wrap-up & Next Steps
Presigned URLs are the correct, zero-infrastructure solution for granting temporary, auditable access to private S3 objects — your backend signs, S3 enforces, and the client downloads directly without any proxy overhead.
Next Steps:
- For uploads (not just downloads), use
ClientMethod='put_object'with the same pattern to generate presigned PUT URLs. - To audit access, enable S3 Server Access Logging or AWS CloudTrail Data Events on your bucket.
- For large-scale distribution, consider pairing presigned URLs with CloudFront signed URLs for CDN-level caching and geo-restriction.
- 📖 Official Docs: AWS — Sharing objects with presigned URLs
Glossary
- Presigned URL: A time-limited URL containing embedded AWS credentials and a cryptographic signature, granting temporary access to a specific S3 object without requiring the requester to have AWS credentials.
- SigV4 (Signature Version 4): AWS's request signing protocol that uses HMAC-SHA256 to authenticate API calls; all presigned URLs use SigV4 by default.
- IAM Least Privilege: The security principle of granting only the minimum permissions required for a task — here, only
s3:GetObjecton the specific key prefix. - STS (Security Token Service): AWS service that issues temporary credentials; if your signing identity uses STS tokens, the presigned URL cannot outlive the token's expiry.
- Canonical Request: A standardized string representation of an HTTP request (method, URI, headers, payload hash) that SigV4 signs to produce the final request signature.
Comments
Post a Comment