1. Introducing Today’s Project!
I’ve worked with web hosting before, but I wanted to understand what hosting looks like when using AWS services directly.
So, for this project, I hosted a static version of my personal links website using Amazon S3.
This was a fairly simple project, but it gave me a much better understanding of S3 buckets, public access, bucket policies, and how AWS decides who can access an object.
⚠️ A small disclaimer before we begin: This article is not meant to be a tutorial, but rather an excessively meticulous dictation of my journey using an S3 bucket. If you’re into that sort of technical BDSM, you’re very welcome to follow my exact steps. Otherwise, please refer to the AWS docs like a good boy.
Project overview
In this project, I will demonstrate how to host a static website on AWS. I’m doing this project to learn how to host a website on amazon S3 so I can dip my toes in AWS services.
In this project, I’m going to host a static website using Amazon Simple Storage Service (S3).
My main goal was to get more comfortable working with AWS services by actually building something instead of only reading about them.
Since S3 can store static files like HTML, CSS, JavaScript, images, and other assets, it can also be used to host a simple static website.
And lucky for me, I already had one lying around.
Tools and concepts
The main AWS service I used was Amazon S3.
Along the way, I also learnt more about:
- S3 buckets and objects
- Static website hosting
- Bucket policies
- Access Control Lists (ACLs)
- Block Public Access settings
- AWS Key Management Service (AWS KMS)
- S3 website endpoints
- HTTP 403 Forbidden errors
Time, challenges, and wins
This project took me approximately 2 hours.
The most challenging part was figuring out why my website kept returning a 403 Forbidden error even though static website hosting was enabled.
The most rewarding part was learning how S3 permissions work and then experimenting with a bucket policy that prevented files from being deleted.
2. How I Set Up an S3 Bucket
Creating the bucket
The first step was creating an S3 bucket to store my website files.
A bucket is basically a container for objects stored in Amazon S3. In this case, my objects would be the HTML, CSS, JavaScript, and other assets that make up my website.
For this project, most of the bucket settings could remain at their defaults.
I also didn’t need to configure AWS KMS manually. S3 encrypts new objects at rest by default using S3-managed encryption keys (SSE-S3). AWS KMS becomes useful when you want more control over the encryption keys used to protect your data.
Creating the bucket itself only takes a few minutes.
It took me around 15 minutes because, naturally, I was reading almost every option and checking the documentation before clicking anything. 😅
Region selection
I created my bucket in the Europe (Stockholm) eu-north-1 Region. At the time, I chose Stockholm thinking it was the closest Region to Kenya. However, AWS also has the Africa (Cape Town) af-south-1 Region, which is geographically closer and would have been another region to consider.
For a production application, I’d choose a Region based on factors like latency, cost, service availability, compliance requirements, and where my users are located.
S3 bucket names are globally unique
Something interesting I learnt during this step is that S3 bucket names must be globally unique.
That means your bucket name can’t already be in use by another AWS customer.
So if you had big dreams of creating my-website, there’s a very good chance somebody got there before you. 😅
I chose links-lomo for my bucket name.
3. Uploading My Website Files to S3
Once the bucket was ready, I needed to upload the actual website.
I used files from an existing static personal website, which made this a good test because the website already had multiple files and folders working together.
I uploaded two folders and five files:
assets/icons/README.mdindex.cssindex.htmlmanifest.jsonservice-worker.js
One thing I wanted to test was whether I needed to zip folders such as my image assets before uploading them.
I didn’t.
S3 let me upload the folders and files directly while preserving their structure.
That was important because files like index.html reference other files such as stylesheets, icons, and images. If those paths don’t match the structure expected by the HTML, parts of the website will break.
4. Enabling Static Website Hosting on S3
Uploading files into a bucket doesn’t automatically turn that bucket into a website.
I still had to enable Static website hosting from the S3 bucket properties.
Website hosting basically means making the website files available through an address that a browser can request.
For S3, enabling static website hosting creates a website endpoint for the bucket.
I also specified my entry document: index.html
At this point, I thought:
Great. Website done.
AWS had other plans. 🥲
My First 403 Forbidden Error
I opened the S3 website endpoint and was greeted by:
403 Forbidden
The website existed.
The endpoint existed.
The files existed.
But the browser still wasn’t allowed to read them.
This turned out to be a really useful lesson about the difference between hosting content and giving someone permission to access that content.
By default, S3 is designed to prevent public access to your data.
My bucket still had Block Public Access enabled, and I hadn’t created a bucket policy that allowed visitors to retrieve my website objects.
So although S3 knew how to host the website, it wasn’t going to casually hand my files over to strangers on the internet.
Fair enough.
5. Making the Website Public
To make the website accessible, I needed to do two things:
- Adjust the bucket’s Block Public Access settings.
- Add a bucket policy allowing the public to read my website files.
For this project, I used the following policy:
{
“Version”: “2012-10-17”,
“Statement”: [
{
“Sid”: “PublicReadGetObject”,
“Effect”: “Allow”,
“Principal”: “**”,
”Action”: ”s3:GetObject”,
”Resource”: ”arn:aws:s3:::links-lomo/**”
}
]
}
Let’s break that down because bucket policies look slightly intimidating the first time you see them.
"Effect": "Allow"
This tells AWS that the action defined in the policy should be permitted.
"Principal": "*"
The * represents everyone.
Since this is a public website, anyone needs to be able to request its files.
"Action": "s3:GetObject"
This allows visitors to retrieve objects from the bucket.
Importantly, I’m not giving random people permission to upload, edit, or delete my files.
I’m allowing them to read them.
"Resource": "arn:aws:s3:::links-lomo/*"
This specifies which resources the permission applies to.
The /* means the policy applies to the objects inside my links-lomo bucket (allow recursive access to all sub-folders).
After saving the policy and reloading the website endpoint…
It worked! 🎉

Understanding Access Control Lists (ACLs)
Another access-control mechanism I came across during the project was Access Control Lists, or ACLs.
ACLs are an older S3 access-control mechanism that can grant basic permissions to buckets and individual objects.
For example, an ACL can specify which AWS accounts or predefined groups are allowed to read or write a particular S3 resource.
However, ACLs are disabled by default for newer S3 buckets when Bucket owner enforced object ownership is being used.
For this project, I didn’t need ACLs.
Instead, I controlled public access using a bucket policy.
Bucket Policies
Bucket policies were probably the most interesting part of this project for me.
A bucket policy is a JSON-based resource policy attached directly to an S3 bucket.
Unlike ACLs, bucket policies can define much more specific rules.
For example, you can control:
- Who can access the bucket
- Which actions they can perform
- Which objects the rule applies to
- Conditions under which access should be allowed or denied
For my static website, the bucket policy allowed everyone to perform:
s3:GetObject
on the objects required by the website.
This is what made the website publicly readable.
6. Experimenting With Delete Protection
Once the website was working, I wanted to experiment a little more with bucket policies.
What would happen if I explicitly prevented object deletion?
I added a rule that denied the s3:DeleteObject action and then tried deleting one of the website files.
AWS immediately returned an access error.
Success!
The policy was doing exactly what I expected: even though I owned the bucket, the explicit deny prevented the delete operation while that policy was active.
I then removed the deny rule when I was done testing.
This experiment helped me understand that bucket policies aren’t only about making resources public or private. They can also be used to control specific actions that someone is allowed to perform.
One More Experiment: Removing Public Read Access
I also wanted to confirm that the bucket policy was actually responsible for making the website accessible.
So I removed the policy that granted public s3:GetObject access and refreshed the website endpoint.
And there it was again:
403 Forbidden
At this point, seeing the error was actually exciting because I knew exactly why it was happening.
I added the public read policy back, refreshed the page, and the website became accessible again.
That small test really helped connect the dots for me:
Browser
↓
S3 Website Endpoint
↓
Does the request have permission?
↓
Bucket Policy
↓
s3:GetObject allowed
↓
Website files returned
Hosting the files was only half of the equation.
Access control determines whether anyone can actually retrieve them.
Final Thoughts
This was a small project, but it was a useful introduction to working with AWS.
I’ve hosted websites plenty of times using platforms that abstract most of this away. With S3, I had to think more explicitly about where the files were stored, how they were exposed to the internet, and which permissions made that possible.
I also really enjoyed deliberately changing policies and seeing how AWS responded. Breaking things on purpose is apparently a very effective learning strategy. 😂
I’d like to give credit to NextWork for walking me through the project and giving me a practical way to explore Amazon S3.
On to the next AWS service! 🚀