CDNs are typically designed to serve public content as fast as possible, so making them serve private content securely is a bit like teaching a sprinter to do ballet – it requires a specialized approach.
Let’s see this in action. Imagine we have a video file, secret_document.mp4, stored in an S3 bucket. We want to serve this file through CloudFront, but only to authenticated users who have paid for access.
First, we need to restrict direct access to the S3 object. In S3, we’ll set the bucket policy to deny public access.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicRead",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-private-bucket/*"
}
]
}
Now, CloudFront needs a way to access this private content. We’ll create an Origin Access Identity (OAI) or Origin Access Control (OAC) for our CloudFront distribution. OAC is the newer, recommended approach.
In the CloudFront console, when configuring your distribution’s origin, select "Origin access" and choose "Origin access control settings (recommended)". Create a new OAC, give it a name like my-private-content-oac, and select "Sign requests (recommended)". This creates a control setting that CloudFront will use.
Next, we need to update our S3 bucket policy to allow the CloudFront OAC to read objects. The console often provides a button to "Copy policy" and then "Update policy" which handles this for you, but manually it looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-private-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_AWS_ACCOUNT_ID:distribution/YOUR_CLOUDFRONT_DISTRIBUTION_ID"
}
}
}
]
}
Crucially, replace YOUR_AWS_ACCOUNT_ID and YOUR_CLOUDFRONT_DISTRIBUTION_ID with your actual AWS account ID and CloudFront distribution ID. This policy explicitly grants s3:GetObject permission to CloudFront for this bucket, but only when the request originates from your specific distribution.
Now, for the private content delivery part. CloudFront uses signed URLs or signed cookies to grant temporary access. Signed URLs are for individual files, while signed cookies can grant access to multiple files. Let’s use signed URLs for our video.
To generate a signed URL, you need to create a policy that specifies the allowed resource, the expiration time, and potentially an IP address range. You’ll then sign this policy with a private key and associate it with a public key in CloudFront.
Here’s a Python example using boto3:
import boto3
from botocore.exceptions import ClientError
from datetime import datetime, timedelta
# Configure your AWS region and bucket name
region_name = "us-east-1"
bucket_name = "your-private-bucket"
object_key = "secret_document.mp4"
distribution_id = "YOUR_CLOUDFRONT_DISTRIBUTION_ID" # Your CloudFront distribution ID
# CloudFront private key and key pair ID (obtained from IAM)
# Ensure you have these configured in your environment or passed securely
private_key = open("your-private-key.pem", "r").read()
key_pair_id = "YOUR_KEY_PAIR_ID"
# Create a CloudFront client
cf_client = boto3.client('cloudfront', region_name=region_name)
# Define the policy
# The resource ARN must match the CloudFront domain name and the object key
policy = {
"Statement": [
{
"Resource": f"https://{distribution_id}.cloudfront.net/{object_key}",
"DateLessThan": datetime.utcnow() + timedelta(hours=1) # URL expires in 1 hour
}
]
}
# Create the signed URL
try:
signed_url = cf_client.create_signed_url(
DistributionID=distribution_id,
Resource={
'url': f"https://{distribution_id}.cloudfront.net/{object_key}",
'date_less_than': datetime.utcnow() + timedelta(hours=1)
},
PrivateKey=private_key,
KeyPairId=key_pair_id,
Policy=policy # Pass the policy here
)
print(f"Signed URL: {signed_url['SignedURL']}")
except ClientError as e:
print(f"Error creating signed URL: {e}")
The key here is that the Resource in the policy refers to the CloudFront distribution’s URL for the object, not the S3 URL. The DateLessThan sets the expiration. When a user accesses this signed URL, CloudFront verifies the signature and the expiration. If valid, it fetches the object from S3 (using the OAC) and serves it to the user.
The most surprising true thing about this mechanism is that the signed URL doesn’t directly grant CloudFront permission to access S3. Instead, it’s a token that proves to CloudFront that you have authorized this specific request for this specific object at this specific time. CloudFront then uses its own pre-configured permissions (via OAC) to fetch the content from the origin.
The next challenge you’ll run into is managing access at a larger scale, beyond individual signed URLs, which leads to exploring signed cookies for granting access to multiple private resources.