Generate and Secure S3 Presigned URLs with Python Boto3
Generate S3 presigned URLs with Python boto3 for downloads and uploads. This guide covers expiration limits, signed parameters, security considerations, and common failures.
Presigned URLs are the standard way to grant temporary, scoped access to a private S3 object without embedding AWS credentials in a client application. In boto3, generating one is a single client call, but the behavior of the resulting URL depends on the client method you choose, the parameters you sign, and the expiration window you set.
What a Presigned URL Actually Contains
A presigned URL is an S3 object URL with a set of authentication query parameters appended. When boto3 generates one, it signs the request using your AWS credentials and embeds the signature in the URL. The resulting URL works only for the exact HTTP method, request parameters, and signed headers that were present when it was created.
The query string typically includes:
X-Amz-Algorithm— alwaysAWS4-HMAC-SHA256for signature version 4X-Amz-Credential— the access key ID and the signing scopeX-Amz-Date— the timestamp at which the URL was signedX-Amz-Expires— the lifetime in secondsX-Amz-SignedHeaders— the names of the headers that are part of the signatureX-Amz-Signature— the HMAC signature derived from your secret access key
If you generate the URL with temporary credentials, the query string also includes X-Amz-Security-Token, and the URL is valid only while those credentials are valid.
Because the signature is derived from your secret access key, S3 can verify the URL without a stored presigned-URL record. Expiration is enforced by the signature timestamp rather than by a server-side entry.
Generating a Presigned URL with boto3
The method you need is generate_presigned_url on a boto3 client, so create the client with boto3.client('s3').
import boto3 s3 = boto3.client('s3') url = s3.generate_presigned_url( ClientMethod='get_object', Params={ 'Bucket': 'my-bucket', 'Key': 'reports/2025/q1.pdf', }, ExpiresIn=3600, ) print(url)
ClientMethod must be the name of a method on the S3 client, such as get_object, put_object, or delete_object. Params must contain exactly the arguments that method accepts. ExpiresIn is the lifetime in seconds.
The returned URL is a full HTTPS endpoint. A browser or any HTTP client can fetch it directly, which is the point: the client never needs AWS credentials.
Downloads vs Uploads: Choosing the Client Method
For read access, sign get_object. For write access, sign put_object. The signed HTTP method is part of the signature, so a URL generated for get_object cannot be used to upload, and a put_object URL applies only to the object key and method that were signed.
upload_url = s3.generate_presigned_url( ClientMethod='put_object', Params={ 'Bucket': 'my-bucket', 'Key': f'uploads/{user_id}/avatar.jpg', 'ContentType': 'image/jpeg', }, ExpiresIn=900, )
When you include ContentType in Params, the client that performs the upload must send the same Content-Type header. If the header differs, S3 rejects the request because the signature no longer matches. The same applies to any other signed parameter, such as ContentLength or Metadata.
For browser-based form uploads, use generate_presigned_post rather than generate_presigned_url. It returns a URL and form fields that let a browser submit a multipart form directly to S3.
How Expiration and Signature Versioning Work
With signature version 4, the maximum ExpiresIn value is 604800 seconds, or seven days. If you need longer-lived access, generate a new URL later, use a different mechanism such as CloudFront signed URLs, or make the object public.
Expiration is enforced by the X-Amz-Date and X-Amz-Expires values. S3 accepts requests that arrive within the period beginning at X-Amz-Date and ending at X-Amz-Date plus the expiration window. If the clock on the generating machine is skewed, or if the request arrives outside that window, S3 rejects the request as expired.
There is no S3 API call to invalidate a presigned URL after it is issued. If a URL leaks, the practical responses are to rotate the signing credentials, revoke the signer's permissions, or, for object-read URLs, delete or rename the target object. Those actions have side effects, so keep expirations short and avoid logging full URLs.
Security Considerations for Presigned URLs
A presigned URL is scoped to the signed operation and object key. S3 authorizes it like any other request from the signing identity. If that identity can call s3:GetObject on the object, the URL can be used to download that object; if it can call s3:PutObject on the key, the URL can be used to upload to that key. The URL does not expand the identity's permissions and does not change bucket policy.
Because the URL contains a valid signature, treat it like a credential:
- Keep expiration short, especially for uploads, to limit the window in which a leaked URL is usable.
- Do not log the full URL in application logs or error reports.
- Generate the URL server-side at request time rather than storing it in a database, unless you have a specific reason to persist it.
- Use a dedicated IAM role with the narrowest policy that the use case requires.
A common mistake is generating a presigned URL with a long expiration and sending it to a client over an unencrypted channel. The URL itself is HTTPS, but if it is delivered through an insecure mechanism, the signature can be captured before the request reaches S3.
Common Failures and How to Diagnose Them
The most frequent failures with presigned URLs fall into a few categories.
Expired URL. The client waits too long before making the request. The fix is to generate the URL closer to the time of use and choose an appropriate ExpiresIn value.
Region mismatch. The boto3 client must be configured for the same region as the bucket. A URL signed with a client pointing at us-east-1 will not work for a bucket in eu-west-1. The region is part of the signature scope.
Parameter mismatch. If the signed Params include ContentType but the client sends a different header, the signature fails. The same happens if the client modifies the URL query string. Every signed parameter and header must match exactly.
Clock skew. If the machine generating the URL has a clock that is significantly off, the signature timestamp may be outside the accepted window. This is more common in containerized or virtualized environments where time synchronization is misconfigured.
When diagnosing, start by comparing the signed parameters with the actual request. The error response from S3 often includes the signature S3 computed and the signature it received, which makes it possible to verify whether the request matched what was signed.
Operational Considerations in Production
In a web application, generate presigned URLs at request time inside the request handler. The boto3 client should be created once and reused, since client construction performs credential resolution and HTTP session setup that does not need to repeat on every request.
Keep the signing identity's permissions minimal. If the application only needs to generate download URLs, the IAM role should allow s3:GetObject on the relevant bucket and nothing else. This limits the damage if the role's credentials are compromised.
For uploads, consider validating the object before it reaches S3. A presigned put_object URL does not enforce file size limits or content type checks beyond what you sign. If you sign ContentLength, the client must send exactly that length, which gives you a degree of control, but it also means the client must know the size in advance.
If you need to revoke access quickly, presigned URLs are the wrong tool. CloudFront signed URLs or a proxy layer that validates requests before forwarding them to S3 give you revocation control. Choose presigned URLs when the simplicity of a self-contained URL outweighs the need for revocation.