Why CloudFront Signed URLs Are Better Than S3 Presigned URLs (And How to Use Them)
When setting up secure access to S3 objects, many AWS users rely on S3 presigned URLs. They’re easy to generate and work well for short-lived access, but they come with hidden pitfalls. If the URL is generated using an IAM role, it can expire when the credentials rotate, often making them unreliable for long-term use.
A better approach is using CloudFront signed URLs. They allow you to control expiration independently of IAM credentials, provide better security, and integrate seamlessly with Origin Access Control (OAC) to ensure CloudFront cannot be bypassed.
Problems with S3 Presigned URLs
A major issue with S3 presigned URLs is that they rely on the IAM role that generates them. If that role is short-lived, as is the case with Lambda functions or ECS tasks, the maximum duration is limited to 12 hours. Even for long-lived IAM users, the presigned URL cannot be valid for more than seven days. If you need a file to be accessible for a month or more, presigned URLs won’t work without complex workarounds.
There’s also a security risk when using presigned URLs without properly configured S3 bucket policies. If the bucket allows direct access, users can bypass CloudFront altogether. This means they could access files directly via S3 URLs, exposing you to unnecessary risk. The correct way to prevent this is by using CloudFront’s OAC, which ensures that objects can only be served through CloudFront.
Setting Up CloudFront Signed URLs with OAC
The first step is to create an S3 bucket and ensure it does not allow public access. When configuring the bucket, block all public access and disable ACLs. Once the bucket is set up, enable Origin Access Control (OAC) in CloudFront. This allows CloudFront to fetch objects from S3 while blocking all direct access.
To enable OAC, create a new CloudFront origin and select your S3 bucket as the source. Under origin settings, choose "Origin access" and select "Use an OAC." CloudFront will generate a policy that needs to be applied to the S3 bucket. This policy ensures that only CloudFront can access the files.
Once OAC is in place, update the S3 bucket policy with the correct permissions:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-bucket-name/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::your-account-id:distribution/your-distribution-id" } } } ] }
This ensures that even if someone gets hold of an S3 URL, they won’t be able to access it directly. The only way to retrieve the file is through CloudFront.
Generating a CloudFront Signed URL
To restrict access to CloudFront, the distribution must be configured to require signed URLs. This can be done by selecting the distribution in the AWS Console and enabling "Restrict Viewer Access" under the Behavior settings.
Next, generate a CloudFront key pair. This key pair will be used to sign URLs and validate requests. You can generate one through the CloudFront Key Pairs section in the AWS Console, and AWS will provide a private key that should be stored securely.
With the key pair created, use the following Python script to generate a signed URL:
import datetime import json import boto3 import rsa import base64 cloudfront_url = "https://your-distribution.cloudfront.net/your-file.pdf" private_key_path = "your-private-key.pem" key_pair_id = "your-key-pair-id" expiration_time = datetime.datetime.utcnow() + datetime.timedelta(days=7) policy = { "Statement": [ { "Resource": cloudfront_url, "Condition": { "DateLessThan": {"AWS:EpochTime": int(expiration_time.timestamp())} } } ] } policy_json = json.dumps(policy).replace(" ", "") signed_policy = base64.b64encode(rsa.sign(policy_json.encode('utf-8'), rsa.PrivateKey.load_pkcs1(open(private_key_path).read()), 'SHA-1')) signed_url = f"{cloudfront_url}?Policy={base64.b64encode(policy_json.encode()).decode()}&Signature={base64.b64encode(signed_policy).decode()}&Key-Pair-Id={key_pair_id}" print("Signed URL:", signed_url)
This signed URL will be valid for seven days, or whatever expiration time is set in the policy. Unlike an S3 presigned URL, this URL remains functional regardless of IAM role rotation.
Why CloudFront Signed URLs Are the Better Choice
For long-lived access, CloudFront signed URLs are more reliable than S3 presigned URLs. They allow precise control over expiration, prevent CloudFront from being bypassed, and integrate better with access logging and security policies.
With OAC enabled, direct access to S3 is blocked, ensuring that files can only be served through CloudFront. This setup prevents unauthorized access while giving full control over how long a file remains available.
If you’re still relying on S3 presigned URLs, now is the time to switch. CloudFront signed URLs provide better security, flexibility, and scalability for any application that needs temporary, controlled access to S3 objects.

In addition to being longer to expire, you also get global performance improvement!