How to Generate an S3 Presigned URL for Temporary Private File Access
When you need to grant a user time-limited, read-only access to a private S3 object — without exposing your AWS credentials or making the bucket public — a Presigned URL is the correct tool. It encodes your authorization directly into a signed, expiring URL that anyone can use, no AWS account required.
TL;DR
| Aspect | Detail |
|---|---|
| What it does | Grants temporary HTTP access to a private S3 object |
| Who signs it | Your backend using IAM credentials (user or role) |
| Expiration range | 1 second to 7 days (604800 seconds max for SigV4) |
| SDK method | generate_presigned_url (Python) / getSignedUrl or GetObjectCommand (Node.js v3) |
| Required IAM permission | s3:GetObject on the target object/prefix |
| No bucket policy change needed | Bucket stays private; the signature carries the authorization |
How It Works: The Architecture
A Presigned URL is not a proxy — it is a cryptographically signed request URL. When your backend generates it, AWS embeds the signing credentials, expiration timestamp, and request parameters into the URL itself using Signature Version 4 (SigV4). The client then presents this URL directly to S3, which re-computes the signature and validates it before serving the object.
(Browser / App)" participant B as "Your Backend
(API Server)" participant SDK as "AWS SDK
(Local Signing)" participant S3 as "Amazon S3" C->>B: GET /download-link?file=report.pdf B->>SDK: generate_presigned_url(bucket, key, ExpiresIn=3600) Note over SDK: Cryptographic signing only.
No network call to S3. SDK-->>B: Signed URL (valid 1 hour) B-->>C: { "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } C->>S3: HTTP GET (Presigned URL) Note over S3: Validates SigV4 signature
and X-Amz-Expires timestamp. S3-->>C: 200 OK — File stream
- Client requests a download link from your backend (e.g., a REST API endpoint).
- Backend calls the AWS SDK using its IAM credentials to generate a presigned URL — no network call to S3 is made at this point; it is a local cryptographic operation.
- Backend returns the URL to the client. The URL contains the bucket, key, expiration, and SigV4 signature as query parameters.
- Client sends a direct HTTP GET to S3 using the presigned URL.
- S3 validates the signature and expiration. If valid, it streams the object directly to the client. Your backend is not in the data path.
Analogy: Think of a Presigned URL like a signed valet ticket. The hotel (your backend) issues a ticket with your signature and an expiry time. The valet (S3) checks the signature and timestamp — if both are valid, they hand over the car (the file). The guest never needs a key to the entire garage.
IAM: Least-Privilege Setup
The IAM principal (user or role) generating the presigned URL must have s3:GetObject permission on the target resource. Grant access to a specific prefix, not the entire bucket.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPresignedDownload",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-private-bucket/downloads/*"
}
]
}
Critical note: If the signing principal is an IAM Role with a session token (e.g., an EC2 instance profile or Lambda execution role), the presigned URL will include the session token. The URL will become invalid if the session expires before the URL's own expiration. Always set your URL expiration shorter than the role's session duration.
Implementation
Python (boto3)
🔽 Click to expand — Python boto3 example
import boto3
from botocore.exceptions import ClientError
def generate_presigned_download_url(
bucket_name: str,
object_key: str,
expiration_seconds: int = 3600
) -> str | None:
"""
Generate a presigned URL to download a private S3 object.
:param bucket_name: Name of the S3 bucket
:param object_key: S3 object key (path within the bucket)
:param expiration_seconds: URL validity in seconds (default: 1 hour)
:return: Presigned URL string, or None on failure
"""
s3_client = boto3.client("s3", region_name="us-east-1")
try:
url = s3_client.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": bucket_name,
"Key": object_key,
},
ExpiresIn=expiration_seconds,
)
return url
except ClientError as e:
print(f"Error generating presigned URL: {e}")
return None
# --- Usage ---
if __name__ == "__main__":
presigned_url = generate_presigned_download_url(
bucket_name="your-private-bucket",
object_key="downloads/report-2024-q4.pdf",
expiration_seconds=3600, # 1 hour
)
if presigned_url:
print(f"Share this URL (valid for 1 hour):\n{presigned_url}")
Node.js (AWS SDK v3)
🔽 Click to expand — Node.js AWS SDK v3 example
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,
expirationSeconds: number = 3600
): Promise {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: objectKey,
});
const signedUrl = await getSignedUrl(s3Client, command, {
expiresIn: expirationSeconds, // 1 hour
});
return signedUrl;
}
// --- Usage ---
(async () => {
const url = await generatePresignedDownloadUrl(
"your-private-bucket",
"downloads/report-2024-q4.pdf",
3600
);
console.log(`Share this URL (valid for 1 hour):\n${url}`);
})();
AWS CLI (Quick Test)
aws s3 presign s3://your-private-bucket/downloads/report-2024-q4.pdf \
--expires-in 3600
What the Presigned URL Looks Like
The generated URL encodes all authorization as query parameters — no custom headers required from the client:
https://your-private-bucket.s3.us-east-1.amazonaws.com/downloads/report-2024-q4.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-signature>
Key Behavioral Gotchas
| Scenario | Behavior |
|---|---|
| URL used after expiration | S3 returns 403 AccessDenied with Request has expired |
| Object deleted after URL generation | S3 returns 404 NoSuchKey |
| IAM role session expires before URL | URL becomes invalid immediately upon session expiry |
Bucket policy explicitly denies s3:GetObject | Presigned URL is rejected — explicit deny overrides the signature |
| Max expiration with SigV4 | 604,800 seconds (7 days); longer values are rejected by S3 |
Wrap-Up & Next Steps
Presigned URLs are the standard, secure pattern for delegating temporary object access without modifying bucket policies or exposing credentials. For production use, consider:
- Logging access: Enable S3 Server Access Logging or AWS CloudTrail Data Events to audit who downloaded what.
- Presigned POST URLs: Use
generate_presigned_post(boto3) to allow users to upload directly to S3 with enforced conditions (file size, content type). - CloudFront Signed URLs: If you need CDN-level caching, geographic restrictions, or sub-second latency, use CloudFront Signed URLs instead.
📖 Official Reference: AWS Docs — Sharing objects with presigned URLs
Glossary
| Term | Definition |
|---|---|
| Presigned URL | A time-limited, signed URL that grants access to a specific S3 object without requiring AWS credentials from the requester. |
| SigV4 (Signature Version 4) | AWS's request signing protocol that cryptographically binds the request parameters, credentials, and timestamp into a tamper-proof signature. |
| IAM Principal | The AWS identity (user, role, or service) whose credentials are used to sign the URL and whose permissions are evaluated by S3. |
| ExpiresIn / X-Amz-Expires | The TTL in seconds embedded in the URL, after which S3 will reject the request regardless of signature validity. |
| Explicit Deny | An IAM or bucket policy Deny statement that overrides any Allow, including the authorization embedded in a presigned URL. |
Comments
Post a Comment