๐ Use CloudFront CDN and S3 Uploads Plugin for WordPress

Zero public access. Zero hotlinking. Maximum speed. Minimum cost.
This is part 2 of our tutorial on how to store media files (images, pdf’s, etc) on S3 using S3 Uploads Plugin. In this tutorial we set up a Cloudfront distribution and instead of loading resources from the S3 bucket, we utilize the Cloudfront CDN for a faster and secure delivery.
๐ Overview
Right now your S3 bucket works โ but it is either:
- Public (which invites hotlinking + cost abuse), OR
- Private (so direct S3 URLs wonโt load on your website).
The correct AWS architecture is:
Visitors โ CloudFront CDN โ (OAC Auth) โ Private S3 Bucket
This gives you:
- โ๏ธ No public S3 access
- โ๏ธ No hotlinking
- โ๏ธ No one stealing your images
- โ๏ธ Faster global delivery (CDN caching)
- โ๏ธ Lower cost
- โ๏ธ Automatic HTTPS
- โ๏ธ Best-practice AWS security
Letโs build this step by step.
๐งฑ 1. Prerequisites
All that we did in the last tutorial
- Your S3 bucket is created (private, bucket-owner-enforced)
- S3 Uploads is fully working in WordPress
- You know your bucket name (e.g.,
nocodeaws-media) - You know your AWS region (e.g.,
us-east-1)
โ๏ธ 2. Create a CloudFront Distribution
Go to:
๐ AWS Console โ CloudFront โ Create Distribution
- Choose Free plan ($0/Month) for now, you can change it later as and when your site gains traffic.
You still pay normal AWS usage (data transfer, requests), but there is no monthly subscription. - Distribution name – Give a name to your distribution e.g.: nocodeaws-s3-cdn (or your domain)
- Domain – If you have a Route 53 managed domain, then its better to add a subdomain now. Enter your website domain name e.g.: nocodeaws.com, replacing nocodeaws with your domain name and click on check domain. If your domain is managed by Route 53, then it will give you an option to add a subdomain. Use something like cdn.nocodeaws.com. Click Next.
- Origin type – Select Amazon S3
- Origin – Click Browse S3 and select the S3 bucket you used in the last tutorial for S3 uploads plugin.
- Settings – check Allow private S3 bucket access to CloudFront : โ๏ธ Enabled
โ This automatically creates an Origin Access Control (OAC) - Click Next
- Enable security – Ignore and click next
- Get TLS certificate – Click Create Certificate. This will create two certificates in AWS ACM. Wait for a few minutes and you should see your certificate there. Click Next.
- Click ๐ Create Distribution
It would take another 5 minutes for your distribution to get ready. You should see the Distribution domain name and ARN. Copy the ARN as we need it in the next step.
๐What we just did
You created a Cloudfront Distribution, which considers your S3 bucket as the source (origin). Cloudfront created SSL certificates for your domain cdn.nocodeaws.com and attached it to the distribution. Cloudfront also uses OAC and made changes to our S3 bucket policy and made it private so that only this cloudfront distribution can access it.
BUT…….
If you remember, in the last tutorial we had updated the bucket policy and allowed public access. Cloudfront doesnt know this, so we need to tweak the bucket policy ourselves in the next step.
๐ชฃ3. Update S3 Bucket policy
Go to:
๐ AWS Console โ S3 โ Select your bucket โ Permissions โ Bucket policy โ Edit
Edit the existing bucket policy to look like this.
Keep in mind to edit the highlighted line and update it with your Cloudfront distribution ARN which you copied in the last step.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::nocodeaws-media/*",
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E367EY4HWZP1WD"
}
}
}
]
}
๐ Replace 123456789012 with your AWS account ID.
Replace E367EY4HWZP1WD with your CloudFront distribution ID.
This makes the bucket:
โ๏ธ Fully private
โ๏ธ But readable by your CloudFront OAC only
Perfect.
๐ 4. Create record in Route 53
Skip this step if you are not using a custom domain like cdn.nocodeaws.com.
When we created this distribution, we also used a subdomain cdn.nocodeaws.com. Cloudfront created the TLS(SSL) certificates for this domain, but did not make a record for it in Route 53. Let’s do that now.
Go to:
๐ AWS Console โ Route 53 โ Hosted zones โ select your domain โ Create Record
- Enter Record name – cdn.nocodeaws.com or what you used earlier.
- Select Alias – Alias to Cloudfront Distribution
- Choose the distribution you just created. Looks something like – d1abcxyz.cloudfront.net
- Click Create Record
๐๏ธ 5. Update S3 Uploads Plugin to Use CloudFront URL
If you are not using a custom domain like cdn.nocodeaws.com, then do this
Get your CloudFront domain:
- CloudFront โ Distribution โ Domain Name
Example:d1abcxyz.cloudfront.net
Then, open Terminal and run
sudo nano /bitnami/wordpress/wp-config.php
Scroll down and edit this line
define( 'S3_UPLOADS_BUCKET_URL', 'https://d1abcxyz.cloudfront.net' );
If you used a custom domain, do this instead
Recommended for better SEO: If you set up a custom domain like cdn.nocodeaws.com, use:
define( 'S3_UPLOADS_BUCKET_URL', 'https://cdn.nocodeaws.com' );
Save and Exit
๐๏ธ6. Restart Bitnami Services
To make sure everything reloads cleanly:
sudo /opt/bitnami/ctlscript.sh restart
This restarts Apache/PHP-FPM and clears opcache.
๐งช7 โ Test End to End
7.1 Upload a New Image in WordPress
- Log into WordPress Admin โ Media โ Add New.
- Upload a JPG/PNG/WebP file.
Check:
- In the Media Library:
- Click the image โ view attachment details.
- The file URL should now be using your CloudFront domain:
https://d123abccloudfront.net/...- or
https://cdn.nocodeaws.com/...if you set that.
- In S3:
- The object appears under
uploads/YYYY/MM/... - Size is non-zero.
- The object appears under
7.2 Test in the browser
Open that media URL in a new tab:
- If it loads via CloudFront: โ success.
- If you try to call the raw S3 URL directly:
- You should get AccessDenied (since bucket is private) โ also good.
โOne Important Warning
When CloudFront takes over, the S3 URLs might break if:
- A plugin uses hard-coded /wp-content/uploads links
- An older URL is stored in WordPress database
โ ๏ธ If you see broken images from older posts, run:
sudo wp search-replace "https://your-bucket.s3.amazonaws.com" "https://cdn.yourdomain.com"This updates all old media URLs.
๐ 8 โ Recommended: Add Hotlink Protection via CloudFront
Right now:
- Your assets are served securely via CloudFront.
- But: If someone discovers your CloudFront URL, they could embed it on their site and you end up paying for the bandwidth (this is called hotlinking).
We can add a simple protection layer using CloudFront Functions.
Hereโs a lightweight CloudFront Function approach.
๐งฎ8.1 Create a CloudFront Function
- Go to CloudFront โ Functions โ Create function.
- Name it:
BlockHotlinking. - Runtime: cloudfront-js-2.0
- Delete default code and Paste this code:
function handler(event) {
var request = event.request;
var headers = request.headers;
var referer = headers.referer;
// Allow requests with no Referer (direct access, some browsers)
if (!referer || !referer.value) {
return request;
}
// Only allow requests from your site
var allowedHost = "nocodeaws.com";
if (referer.value.indexOf(allowedHost) === -1) {
return {
statusCode: 403,
statusDescription: "Forbidden"
};
}
return request;
}
- Save and Publish the function in the live environment.
๐8.2 Attach Function to Your Distribution
- Go to CloudFront โ Distributions โ your distribution.
- Click the Behaviors tab.
- Edit the Default behavior.
- Under Function associations, choose:
- Event type: Viewer request
- Function type: Cloudfront Function
- Function Name:
BlockHotlinking
- Save.
Now:
- If a request comes with a Referer that doesn’t contain your domain, CloudFront returns 403 Forbidden.
- If the Referer is your own site (or no Referer), it passes.
Note: Itโs not bulletproof against deliberate spoofing, but it stops 99% of casual hotlinking.
โฐ9 โ Cache Invalidation Basics
When you change things like:
- CSS/JS
- Image names
- Important static content
You might need to invalidate CloudFrontโs cache.
Go to:
CloudFront โ Your distribution โ Invalidations โ Create invalidation
Paths can be:
/wp-content/*/uploads/*- Or a specific file
/uploads/2025/10/my-image.jpg
Invalidations cost a bit at scale, but for small sites and targeted paths, itโs usually cheap/within free quotas.
10. How to properly test that hotlink protection works
To verify that your protection is actually active:
Create a simple HTML file on your local machine (Change the highlighted URL with your new image url) and save it as test.html:
<!DOCTYPE html> <html> <body> <h1>Hotlink test</h1> <img src="https://cdn.nocodeaws.com/path/to/one-of-your-images.jpg"> </body> </html>
To truly simulate hotlinking, the HTML file must be served over HTTP from a different origin. To do that :
Open terminal, goto the folder where you saved test.html in your local machine and run
# cd c:/go to the local machine folder where you saved test.html
python -m http.server 8000
If python is not installed in your local machine, run
python
This will install python on your local machine. Once done re-run
python -m http.server 8000
Then open http://127.0.0.1:8000/test.html in your browser.
The image should not load. If that’s the case, hotlink protection works.๐ฅณ
๐11. Recap: What You Have Now
Youโve successfully wired:
- WordPress (Bitnami Lightsail)
- S3 Uploads plugin
- Amazon S3 (private, bucket-owner-enforced)
- CloudFront CDN (with Origin Access Control)
Your system now:
- ๐ฏ Keeps S3 fully private (no public buckets required)
- ๐ Uses CloudFront for global, cached delivery
- ๐ Restricts S3 access only to CloudFront (via OAC)
- ๐งฐ Lets S3 Uploads generate CloudFront URLs for all media
- ๐ก๏ธ Can optionally block hotlinking using a CloudFront Function
- ๐ธ Reduces origin (S3) bandwidth and cost
This is a production-grade, AWS-aligned architecture that you can proudly document on NoCodeAWS.


