Why CloudFront Signed URLs Are Better Than S3 Presigned URLs

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.

AWS S3 presigned URLs vs Cloudfront presigned URLs (longer expiration possible)

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

Overwhelmed by AWS?

Struggling with infrastructure? We streamline your setup, strengthen security & optimize cloud costs so you can build great products.

Related AWS best practices blogs

Looking for more interesting AWS blog posts?

How to ensure your AWS account is not compromised

Read more to learn the different ways your AWS account can get compromised, how to avoid it, and what to do if you suspect it is compromised.

Read more
build process

How to overcome "Unsupported Wildcard In Principal"

If you want to create an policy that wildcards the Principal AWS element in an IAM trust policy you will get an error.

Read more

How to Reduce AWS Lambda Costs Without Hurting Performance

Optimizing AWS Lambda costs isn’t just about cutting memory—sometimes, the smartest move is allocating more. Learn when a bigger Lambda is better and when to ditch it for ECS.

Read more

Understanding metadata endpoints and their role in AWS applications

In this blog we dive into detailed usage of the metadata endpoints of ECS. Crucial for understanding how authentication works through official AWS SDKs.

Read more

Verifying S3 Gateway Endpoints: Why AWS Should Make It Easier

AWS recommends using traceroute to verify S3 Gateway Endpoints, but isn't there a better way?

Read more

Why do S3 pre signed URLs expire after 12 hours, despite setting a longer duration?

S3 objects can be requested through a so called pre signed URLs, however the pre signed URL is tied to the identity that generated the URL. This means that if the credentials expire that generated thi ...

Read more

You do not need that bastion host, there are better alternatives

This article discusses why you do not need that bastion host and what the alternatives are. Do you have any further questions after reading this article? If so, please contact me.

Read more