Creating an S3 Presigned URL: Temporary Access to Private Files with a 1-Hour Expiration
An S3 presigned URL lets you grant time-limited, unauthenticated download access to a private object — no bucket policy changes, no IAM user required.
TL;DR: Creating an S3 Presigned URL
| Step | What to Do | Key Detail |
|---|---|---|
| 1 | Verify IAM caller permissions | Caller must have s3:GetObject on the target object |
| 2 | Confirm bucket-level access controls | Block Public Access does NOT block presigned URLs; bucket policy Deny rules do |
| 3 | Generate the presigned URL via SDK | Use generate_presigned_url (Python) or GetObjectCommand + getSignedUrl (Node.js) |
| 4 | Validate expiration and delivery | Set ExpiresIn to 3600 seconds; URL is signed at generation time |
| 5 | Audit and rotate signing credentials | URL validity is tied to the signing credential — IAM key revocation invalidates the URL immediately |
How S3 Presigned URLs Work
A presigned URL encodes a time-bounded, cryptographically signed authorization directly into the URL itself. The signature is computed using the credentials of the IAM principal that generates it — whether that is an IAM user with long-term access keys or, preferably, an IAM role with temporary credentials from AWS STS. When a recipient makes an HTTP GET request to the URL, S3 validates the embedded signature and expiration before serving the object. No AWS credentials are required on the recipient's side.
The access check happens at request time, not at generation time.
Think of a presigned URL like a signed check: the bank (S3) honors it only if the issuer's account (IAM principal) still has funds (valid permissions) and the check hasn't expired — regardless of who presents it at the counter.
- Application calls the AWS SDK using an IAM role or user credentials to generate a presigned URL for a specific S3 object.
- SDK computes the signature locally — no AWS API call is made during generation.
- Application delivers the URL to the end user (e.g., via API response or email).
- User makes an HTTP GET directly to S3 using the presigned URL.
- S3 validates the signature and expiration, then serves the object if both checks pass.
Step 1: Verify IAM Caller Permissions for the S3 Presigned URL
The IAM principal generating the URL must hold s3:GetObject permission on the target object at the moment the recipient uses the URL. If the principal's permissions are revoked after generation, the URL stops working — even if it has not expired.
— Why this step: the SDK generates the URL locally without contacting S3, so a misconfigured IAM policy produces no error at generation time — the failure surfaces only when the recipient tries to use the URL.
Attach a least-privilege policy to the role or user generating the URL:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPresignedGetObject",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/private-files/*"
}
]
}
Verify the effective permissions of the calling identity before proceeding:
aws sts get-caller-identity
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/MyAppRole \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::example-bucket/private-files/report.pdf
Step 2: Confirm Bucket-Level Access Controls
Production Gotcha: A team enables S3 Block Public Access on a bucket and assumes presigned URLs will also stop working. The URLs keep functioning. Later, they add an explicit Deny in the bucket policy to block a specific IP range — and presigned URLs for all users in that range silently return 403, with no error at generation time and no SDK exception to catch.
— Why this step: S3 Block Public Access settings do not block presigned URL access — they control ACL-based and policy-based public access. An explicit Deny in a bucket policy, however, overrides any presigned URL regardless of the signing principal's IAM permissions.
Check for bucket-level policies that could override the presigned URL:
aws s3api get-bucket-policy \
--bucket example-bucket \
--query Policy \
--output text
aws s3api get-bucket-policy-status \
--bucket example-bucket
Inspect Block Public Access settings (these do not affect presigned URLs, but confirm your mental model):
aws s3api get-public-access-block \
--bucket example-bucket
An explicit Deny in a bucket policy always wins — even a validly signed URL returns 403 if a Deny statement matches the request.
Step 3: Generate the S3 Presigned URL Using the AWS SDK
URL generation is a local SDK operation. The SDK signs the request parameters using the caller's credentials and returns a URL string. No S3 API call is made during this step. The expiration is embedded in the URL as a query parameter and is evaluated by S3 at request time.
— Why this step: choosing the correct SDK method matters — some SDK versions and languages have separate presigned URL utilities that handle Signature Version 4 correctly; using a raw HTTP signing approach risks generating malformed URLs that S3 rejects with a cryptic SignatureDoesNotMatch error.
Here's what goes through an engineer's mind before writing this code: Is my Lambda or EC2 instance using an IAM role with temporary credentials? If so, the presigned URL will embed the session token as a query parameter automatically. I need to make sure the role's session doesn't expire before the URL does — otherwise the URL becomes invalid mid-window even though the expiration timestamp hasn't passed yet.
Python (boto3):
import boto3
s3_client = boto3.client('s3', region_name='us-east-1')
presigned_url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': 'example-bucket',
'Key': 'private-files/report.pdf'
},
ExpiresIn=3600
)
print(presigned_url)
Node.js (AWS SDK v3):
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const client = new S3Client({ region: 'us-east-1' });
const command = new GetObjectCommand({
Bucket: 'example-bucket',
Key: 'private-files/report.pdf',
});
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
console.log(presignedUrl);
The ExpiresIn value is in seconds. A value of 3600 produces a URL valid for exactly one hour from the moment of generation.
Step 4: Validate Expiration and URL Delivery
— Why this step: the expiration clock starts at generation time, not delivery time — if your application queues the URL for asynchronous delivery (e.g., via SQS or email), queue latency reduces the effective window the recipient has to use it.
Test the generated URL immediately after generation to confirm the object is accessible:
curl -I "<paste-presigned-url-here>"
A successful response returns HTTP 200 OK with Content-Type and Content-Length headers. A 403 Forbidden response indicates a permission or policy problem. A 400 Bad Request with RequestExpired in the body confirms the URL has passed its expiration window.
In practice, teams often set ExpiresIn to a very large value (days or weeks) to avoid user complaints about expired links. This widens the window for URL leakage — a leaked presigned URL grants full object access until expiration or credential revocation. Size the expiration to the minimum window your workflow requires.
Step 5: Audit and Rotate Signing Credentials
— Why this step: a presigned URL's validity is coupled to the signing credential's validity — revoking the IAM access key or role session that signed the URL immediately invalidates all URLs signed by that credential, which is both a security lever and an operational risk if credentials are rotated without accounting for outstanding URLs.
Identify which credential type is signing your URLs:
aws sts get-caller-identity
If the output shows an assumed-role ARN (e.g., arn:aws:sts::123456789012:assumed-role/MyAppRole/session-name), the URL is signed with temporary STS credentials. The URL will become invalid when the role session expires — even if ExpiresIn has not elapsed. Ensure the role's maximum session duration covers the intended URL lifetime.
Check the maximum session duration for a role:
aws iam get-role \
--role-name MyAppRole \
--query 'Role.MaxSessionDuration'
For long-lived presigned URLs, prefer IAM roles with a session duration configured to exceed the URL expiration window. Avoid signing presigned URLs with long-term IAM user access keys — use IAM roles with temporary credentials wherever possible.
- IAM Role (STS session): URL validity is bounded by the lesser of
ExpiresInand the remaining session duration. This is the recommended signing credential type. - IAM User (long-term key): URL validity is bounded only by
ExpiresIn, but long-term keys carry higher credential exposure risk. - Credential revocation: Deactivating or deleting the signing key immediately invalidates all outstanding URLs signed by that key — useful as an emergency revocation mechanism.
Common Mistakes When Generating S3 Presigned URLs
In practice, teams often generate presigned URLs from a Lambda function that assumes a role with a 1-hour session duration and simultaneously set ExpiresIn=3600. If the Lambda invocation happens mid-session, the URL may expire in as little as a few minutes — not one hour — because the STS session clock started before the URL was generated. The fix is to ensure the role's session duration is set to a value that comfortably exceeds the intended URL lifetime, or to generate a fresh STS session specifically for URL signing.
A second common error: generating the URL in one AWS region while the bucket resides in another, without specifying the correct region in the SDK client. This produces a SignatureDoesNotMatch error because the signing region embedded in the URL does not match the bucket's region. Always instantiate the SDK client with the explicit region of the bucket.
Glossary
- Presigned URL
- A time-limited URL that grants access to a private S3 object by embedding a cryptographic signature and expiration in the URL query parameters.
- ExpiresIn
- SDK parameter specifying the URL's validity window in seconds, measured from the moment of generation.
- STS (Security Token Service)
- AWS service that issues temporary, scoped credentials for IAM roles. Presigned URLs signed with STS credentials inherit the session's expiration as an upper bound.
- Signature Version 4 (SigV4)
- The AWS request signing protocol used to authenticate presigned URL requests. The AWS SDK handles SigV4 computation automatically.
- s3:GetObject
- The IAM action required on the target object for a presigned URL to succeed at request time.
Comments
Post a Comment