Host a Static Website on Amazon S3

Configure S3 buckets, public access policies, and CORS for static website hosting.

Introduction

30 Second Summary

Every website needs a home. When you type a URL into your browser, the files that make up that page have to live somewhere that's always available and fast to reach.

In this project, you will host a static website entirely on Amazon S3, configure public access through bucket policies, and set up Cross-Origin Resource Sharing between two S3 buckets.

What You'll Build

A static website hosted on Amazon S3 with proper public access configuration, cross-origin resource sharing, and a custom error page across two S3 buckets.

By the end of this project, you'll have:

  • A primary S3 bucket configured as a static website with index and error documents.
  • A bucket policy granting public read access with all four Block Public Access settings understood and configured.
  • A working CORS configuration allowing cross-origin requests between two S3 buckets, verified in the browser.
  • Secret Mission: Add redirect routing rules that simulate a real-world site migration.

Are there any prerequisites?

You need an AWS account with console access. All work in this project happens in the AWS Management Console and your web browser. No CLI or coding experience is required.

Verify AWS Console Access and Choose a Region

Before you can build anything on S3, you need to confirm your AWS Management Console access is working and choose where your data will live.

S3 uses a security-by-default approach that blocks all public access at the account level. In this step, you'll sign in, pick a region, and temporarily disable those account-level restrictions so you can create a public website later.

In this step, get ready to:
  • Sign in to the AWS Management Console and confirm S3 access.
  • Choose a region for all your lab resources.
  • Disable account-level Block Public Access settings.
Sign in to the S3 dashboard
  • Open your web browser (Chrome or Edge).
  • Navigate to https://console.aws.amazon.com/s3/ and sign in with your AWS credentials.
  • Confirm you can see the S3 dashboard with the Buckets section listed.

What should I see?

You should see the S3 dashboard with a list of buckets (which may be empty if this is a new account). You might also see a banner at the top mentioning that Block Public Access is enabled at the account level. Note this for later.

Can't access the S3 dashboard?

If you see an "Access Denied" or permissions error, your IAM user may not have S3 access. Check with your account administrator that your user has the necessary S3 permissions.

help me troubleshoot AWS Console access for S3

Choose a region for this lab

S3 bucket names are globally unique across all AWS accounts. However, the data inside a bucket is stored in the specific region you choose. All resources in this lab should be in the same region.

  • Look at the top-right corner of the AWS Console. You'll see the currently selected region.
  • Click the region name to open the region selector dropdown.
  • Select US East (N. Virginia) us-east-1 or any region close to you.

Why does region matter?

Your bucket name must be unique across all of AWS, not just your account. Two people cannot create a bucket with the same name, even in different regions.

The data stored in your bucket stays in the region you select. Choosing a region close to your users reduces latency. For this lab, us-east-1 is a good default because it has the broadest service support.

Disable account-level Block Public Access

AWS enables Block Public Access at the account level by default. This prevents any bucket in your account from being made public. For this lab, you need to disable it so your website buckets can serve content to the internet.

  • In the left-hand navigation of the S3 console, click Block Public Access settings for this account.
  • Click Edit.
  • Uncheck Block all public access. This unchecks all four individual settings beneath it.
  • Click Save changes.
  • A confirmation dialog appears. Type confirm in the text field and click Confirm.

You should see a green success banner confirming the settings have been saved. All four Block Public Access settings now show as Off.

What are the four Block Public Access settings?

S3 has four independent settings that control public access at the account and bucket levels:

  • BlockPublicAcls blocks PUT calls that include public ACLs.
  • IgnorePublicAcls causes S3 to ignore any existing public ACLs on the bucket and its objects.
  • BlockPublicPolicy rejects bucket policies that grant public access.
  • RestrictPublicBuckets restricts public bucket access to only AWS service principals and authorized users.

S3 always applies the most restrictive combination of account-level and bucket-level settings. All four must be off at the account level for a public website to work.

Remember to re-enable this after the lab

Disabling Block Public Access at the account level means any bucket you create can potentially be made public. You will re-enable these settings in the Before You Go section to restore your account's default security posture.

Seeing an error about organization policies?

If you see a message that your account is managed by an organization-level Block Public Access policy, your AWS Organization administrator has locked these settings. Contact your admin to proceed, or use a personal AWS account for this lab.

help me resolve Block Public Access organization policy restrictions

Your AWS environment is ready and your account now allows public access policies. Next up, you'll create your first S3 bucket and explore how S3 stores objects.

Create the Primary Bucket and Explore S3 Fundamentals

Your console is ready and your region is set. Now it's time to create the container where your website files will live.

In S3, everything starts with a bucket. A bucket is the top-level container that holds your files (called objects). In this step, you'll create your first bucket, upload a file, and discover how S3 keeps everything private by default.

In this step, get ready to:
  • Create your primary S3 bucket with the right settings for a public website.
  • Upload a test file and explore its object properties.
  • Confirm that S3 blocks public access by default.
Create the primary bucket

Every S3 bucket name must be globally unique across all AWS accounts worldwide. Think of it like a domain name. No two people can have the same bucket name.

  • In the S3 dashboard, click Create bucket.
  • In the Bucket name field, enter a unique name using this format: your-name-website-lab-2026 (replace your-name with your actual name, all lowercase, no spaces).
  • For AWS Region, confirm your chosen region is selected (e.g., US East (N. Virginia) us-east-1).
  • Under Object Ownership, leave the default: ACLs disabled (Bucket owner enforced).
  • Scroll to Block Public Access settings for this bucket.
  • Uncheck Block all public access.
  • Check the acknowledgement box that appears below the settings.
  • Leave all other settings at their defaults (Bucket Versioning disabled, Default encryption SSE-S3).
  • Scroll to the bottom and click Create bucket.

Why is Block Public Access unchecked?

S3 enables all four Block Public Access settings by default on every new bucket. This is a safety net that prevents accidental public exposure.

For a static website, you need the public to read your files. Disabling Block Public Access at the bucket level allows you to attach a public bucket policy later. You already disabled it at the account level in the previous step, and now you are also disabling it for this specific bucket.

What is a globally unique bucket name?

S3 bucket names share a single global namespace across every AWS account in the world. If someone else already has my-website, you cannot use that name.

Including your name and a date (like jane-website-lab-2026) makes collisions unlikely. The bucket data itself is stored only in the region you selected.

Bucket name already exists?

If you see an error saying the name is already taken, try adding extra characters like your birth year or a random number to the end of the name.

Help me pick a unique S3 bucket name.

Upload a test file and examine object properties

S3 stores data as objects inside buckets. Each object has a key (its full name/path), metadata, and the data itself. Let's upload a file and see these properties firsthand.

  • Click on your newly created bucket name to open it.
  • Click Upload.
  • Click Add files and select any small text file from your computer (a .txt file with a few words works perfectly).
  • Click Upload at the bottom of the page.
  • Once the upload completes, click Close.
  • Click on the uploaded file's name to open its detail page.

You should now see the object's properties panel. Take a moment to look at each field.

  • Find the Key field. This is the object's full name (including any folder-like prefixes).
  • Find Size, Last Modified, and Storage class (Standard).
  • Find Server-side encryption (SSE-S3) and ETag.

What is S3's flat namespace?

S3 does not actually have folders. It uses a flat namespace where the object key is the entire path. A key like images/logo.png is just a single string that happens to contain a forward slash.

The S3 console shows what looks like folders, but this is just a visual convenience. Under the hood, every object sits at the same level inside the bucket.

Test S3's security-by-default behavior

Even though you unchecked Block Public Access when creating the bucket, your objects are still not publicly accessible. Block Public Access is just one layer of S3's security model. Without a bucket policy explicitly granting access, nobody outside your account can read your files.

  • On the object detail page, scroll to the Object overview section.
  • Find the Object URL field and copy the URL.
  • Open a new browser tab and paste the Object URL into the address bar.
  • Press Enter to navigate to the URL.

You should see an XML error page with the code AccessDenied. This confirms that S3 keeps objects private by default, even when Block Public Access is off.

Why is access still denied?

Disabling Block Public Access removes one barrier, but it does not grant anyone access on its own. Think of it like unlocking the front door without actually opening it.

You still need a bucket policy that explicitly says "allow the public to read objects in this bucket." You will write that policy in a later step.

Not seeing an Access Denied error?

If the file downloads or displays instead of showing an error, your bucket may have a legacy public ACL applied. Go back to the bucket's Permissions tab and confirm Object Ownership is set to Bucket owner enforced (ACLs disabled).

Help me figure out why my S3 object is publicly accessible when it shouldn't be.

You have just seen S3's security model in action. Next up, you will enable static website hosting on this bucket and upload your actual site files.

Enable Static Website Hosting and Upload Site Files

Your primary bucket exists and stores objects, but it does not serve web pages yet. S3 has a built-in feature called static website hosting that turns a bucket into a basic web server.

In this step, you will create your site's HTML files locally, enable static website hosting on the bucket, and upload the files. You will also see why the site still returns a 403 error until you configure a bucket policy in the next step.

In this step, get ready to:
  • Create index.html and error.html files locally.
  • Enable static website hosting on your primary bucket.
  • Upload the HTML files and test the website endpoint.
Create your HTML files locally

A static website needs at least two pages: a homepage that visitors land on and an error page for broken links. S3 calls these the Index document and the Error document.

  • Open your text editor (Notepad or VS Code).
  • Create a new file and paste the following code. Save it as index.html on your Desktop:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My S3 Static Website</title>
</head>
<body>
    <h1>Welcome to My S3 Static Website</h1>
    <p>This website is hosted entirely on Amazon S3 with static website hosting enabled.</p>
    <p>S3 provides 99.999999999% (11 nines) durability and strong read-after-write consistency.</p>
    <h2>Data from this bucket:</h2>
    <pre id="data-output">Loading...</pre>
    <script>
        fetch('data.json')
            .then(response => response.json())
            .then(data => {
                document.getElementById('data-output').textContent = JSON.stringify(data, null, 2);
            })
            .catch(error => {
                document.getElementById('data-output').textContent = 'Error loading data: ' + error.message;
            });
    </script>
</body>
</html>

What does this code do?

This HTML file is your website's homepage. It displays a heading, two paragraphs about S3, and a <script> tag that will fetch data from a JSON file later in the project.

The script will show "Loading..." initially. It will not work until you upload data.json in a later step.

  • Create another new file and paste the following code. Save it as error.html on your Desktop:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Page Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p>The page you are looking for does not exist.</p>
    <p><a href="/">Return to Homepage</a></p>
</body>
</html>

What does this code do?

This is your custom 404 error page. S3 will display it whenever a visitor navigates to a path that does not exist in your bucket. It includes a link back to the homepage.

  • Confirm that both index.html and error.html are saved on your Desktop.

Can't find the saved files?

Make sure you saved with the .html extension, not .txt. In Notepad, change the "Save as type" dropdown to "All Files" before saving.

help me save HTML files from Notepad with the correct extension

Enable static website hosting on your bucket

By default, S3 buckets only respond to API requests. Enabling static website hosting adds a public HTTP endpoint that serves your HTML files like a web server.

  • In the S3 console, click on your primary bucket name to open it.
  • Click the Properties tab.
  • Scroll down to the Static website hosting section.
  • Click Edit.
  • Select Enable.
  • Select Host a static website.
  • Enter index.html in the Index document field.
  • Enter error.html in the Error document field.
  • Click Save changes.

Why do we need both an index and error document?

The index document is the default page S3 returns when someone visits the root of your site (like visiting a homepage). The error document is what S3 returns when someone requests a path that does not match any object in the bucket.

Both names are case-sensitive. They must exactly match the file names you upload to the bucket.

  • After saving, scroll back to the Static website hosting section on the Properties tab.
  • Find the Bucket website endpoint URL displayed at the bottom of this section.
  • Copy this URL and save it somewhere you can find later (a sticky note, a new browser tab, or a text file).

What does the website endpoint look like?

The endpoint format depends on your region. For us-east-1, it looks like:

http://your-bucket-name.s3-website-us-east-1.amazonaws.com

Some regions use a dot instead of a dash (e.g., s3-website.eu-west-1). S3 website endpoints do not support HTTPS. They only support GET and HEAD requests on objects.

Don't see the website endpoint?

Make sure you clicked Save changes after enabling static website hosting. The endpoint only appears after the setting is saved.

Also confirm you selected Enable and Host a static website in the configuration form.

help me find my S3 static website hosting endpoint

Upload the HTML files and test the endpoint
  • Click the Objects tab in your bucket.
  • Click Upload.
  • Click Add files.
  • Select both index.html and error.html from your Desktop.
  • Click Upload at the bottom of the page.
  • Wait for the success message, then click Close.

You should see index.html and error.html listed alongside your test file in the Objects tab.

  • Open a new browser tab and paste your bucket website endpoint URL.
  • Press Enter to visit the page.

You will see a 403 Forbidden error. This is expected.

Why does it say 403 Forbidden?

Static website hosting gives your bucket a public URL, but it does not automatically make the objects readable. S3 is secure by default. You still need a bucket policy that explicitly grants public read access.

Think of it like putting a building's address on a map. People can find it, but the door is still locked until you hand out keys.

Seeing a different error or a blank page?

Make sure you are visiting the website endpoint (contains s3-website in the URL), not the S3 object URL or REST API endpoint. The object URL has a different format and returns XML errors.

Also confirm that index.html is uploaded to the bucket root (not inside a folder).

help me troubleshoot my S3 website endpoint

Your static site is hosted but locked down. Next up, you will configure Block Public Access settings and write a bucket policy to let the world see your website.

Configure Block Public Access and Write a Bucket Policy

Your static website hosting is enabled and your files are uploaded, but visitors still see a 403 Forbidden error. That is because S3 blocks all public access by default.

In this step, you will understand the four Block Public Access settings, then write a bucket policy that grants public read access to your website files. Once the policy is in place, your website will load for anyone with the URL.

In this step, get ready to:
  • Review the four Block Public Access settings and understand what each one controls.
  • Write and apply a bucket policy granting public read access.
  • Verify your website loads and your custom error page works.
Review the Block Public Access settings

S3 has four independent settings that control how public access can be granted to a bucket. You already unchecked these when creating the bucket, but understanding what each one does is critical for the SAA-C03 exam.

  • Click on your bucket name to open it.
  • Click the Permissions tab.
  • Scroll down to the Block public access (bucket settings) section.
  • Confirm that all four settings are set to Off.

What are the four Block Public Access settings?

Each setting controls a different aspect of public access. S3 applies the most restrictive combination of bucket-level and account-level settings.

  • BlockPublicAcls blocks PUT Bucket ACL, PUT Object ACL, and PUT Object calls if the specified ACL is public.
  • IgnorePublicAcls causes S3 to ignore all public ACLs on the bucket and objects in the bucket.
  • BlockPublicPolicy causes S3 to reject calls to PutBucketPolicy if the specified bucket policy allows public access.
  • RestrictPublicBuckets restricts access to a bucket with a public policy to only AWS service principals and authorized users within the bucket owner's account.

All four must be off to allow a public website without CloudFront.

Write and apply the bucket policy

A bucket policy is a JSON document that defines who can access your bucket and what actions they can perform. You need a policy that grants s3:GetObject permission to everyone.

  • Still on the Permissions tab, scroll down to the Bucket policy section.
  • Click Edit.
  • Paste the following policy into the policy editor, replacing the bucket name in the Resource ARN with your actual bucket name:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::[[BUCKET_NAME="your-bucket-name"]]/*"
            ]
        }
    ]
}
  • Click Save changes.

What does this policy do?

  • "Principal": "*" means anyone on the internet can perform the allowed action.
  • "Action": ["s3:GetObject"] allows only reading objects. Visitors cannot upload, delete, or list bucket contents.
  • The /* at the end of the Resource ARN applies the permission to all objects in the bucket.

How are bucket policies different from ACLs?

Bucket policies are JSON-based, resource-level, and support conditions (like restricting by IP address or time of day). ACLs are a legacy XML-based mechanism with limited granularity.

AWS recommends disabling ACLs entirely and using bucket policies instead. That is why your bucket was created with the "Bucket owner enforced" Object Ownership setting, which turns off ACLs by default.

Seeing an Access Denied error when saving?

If the console rejects your policy, check that account-level Block Public Access is disabled. S3 applies the most restrictive combination of account-level and bucket-level settings.

Also verify your Resource ARN matches your exact bucket name, including any hyphens or numbers.

Help me fix this bucket policy error.

Verify your website loads

With the bucket policy applied, your website endpoint should now serve pages to anyone.

  • Navigate to your bucket's Properties tab.
  • Scroll to Static website hosting at the bottom.
  • Click the bucket website endpoint URL to open it in a new tab.

You should see your index.html page with the heading "Welcome to My S3 Static Website".

  • Test your custom error page by adding a non-existent path to the website URL (e.g., /nonexistent-page).

You should see your custom error.html page with the heading "404 - Page Not Found" and a link back to the homepage.

Why does the website endpoint serve error.html?

The website endpoint is special. When a requested object does not exist, S3 returns your configured error document instead of a raw XML error response. This only happens on the website endpoint, not the standard REST API endpoint.

Still seeing 403 Forbidden?

Make sure you saved the bucket policy successfully. Go back to the Permissions tab and confirm the policy JSON appears under Bucket policy.

Double check that the Resource ARN in your policy ends with /* (not just the bucket name without the wildcard).

Help me troubleshoot my 403 error on S3 static website.

Your website is now publicly accessible. Next up, you will create a secondary bucket and configure CORS to allow cross-origin requests between the two buckets.

Create a Secondary Bucket and Configure CORS

Your primary bucket is serving a public website. Now you need to prove that S3 enforces browser security rules when one website tries to fetch resources from another.

In this step, you will create a second S3 bucket that hosts its own page. That page will attempt to fetch a file from your primary bucket. This cross-origin request will fail until you explicitly allow it with a CORS configuration.

In this step, get ready to:
  • Create a secondary S3 bucket with public access and static website hosting.
  • Upload files that trigger a cross-origin fetch request.
  • Configure CORS on the primary bucket to allow the request.
Create the secondary bucket with public access

You need a separate origin to demonstrate CORS. A second S3 bucket with its own website endpoint acts as that separate origin.

  • Navigate back to the S3 dashboard at https://console.aws.amazon.com/s3/.
  • Click Create bucket.
  • Enter a globally unique name for the secondary bucket (e.g., your-name-assets-lab-2026).
  • Select US East (N. Virginia) us-east-1 as the region.
  • Scroll down to Block Public Access settings for this bucket.
  • Uncheck Block all public access.
  • Check the acknowledgment box that appears.
  • Leave all other defaults and click Create bucket.

Now add the same public read bucket policy you used on the primary bucket.

  • Click your new secondary bucket to open it.
  • Go to the Permissions tab.
  • Scroll to Bucket policy and click Edit.
  • Paste the following policy, replacing the bucket name in the Resource ARN with your secondary bucket's name:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::[[SECONDARY_BUCKET="your-name-assets-lab-2026"]]/*"
            ]
        }
    ]
}

What does this policy do?

This is the same policy structure you applied to your primary bucket. It grants s3:GetObject permission to all principals (*), meaning anyone can read objects from this bucket over the web.

  • Click Save changes.

Now enable static website hosting on this bucket.

  • Go to the Properties tab.
  • Scroll to Static website hosting and click Edit.
  • Select Enable.
  • Choose Host a static website.
  • Enter index.html as the Index document.
  • Click Save changes.
  • Copy the Bucket website endpoint displayed at the bottom of the Static website hosting section. You will need this URL in the next substep.

Don't see the website endpoint?

Make sure you clicked Save changes after enabling static website hosting. Scroll back to the bottom of the Static website hosting section on the Properties tab to find the endpoint URL.

help me find the website endpoint for my S3 bucket

Upload files for the cross-origin test

You need two new files. A data.json file on the primary bucket gives the secondary bucket something to fetch. An index.html on the secondary bucket makes the cross-origin request.

  • Open your text editor and create a new file called data.json with the following content:
{
    "message": "Hello from S3!",
    "service": "Amazon S3",
    "durability": "99.999999999%",
    "consistency": "strong read-after-write"
}

What is this file for?

This is a simple JSON file that your secondary bucket's page will try to fetch. It contains basic info about S3 to display once the cross-origin request succeeds.

  • Save data.json to your computer.
  • In the S3 console, navigate to your primary bucket.
  • Click Upload, then Add files.
  • Select data.json from your computer and click Upload.

Now create the secondary bucket's homepage. This page uses JavaScript fetch() to request data.json from your primary bucket's website endpoint.

  • In your text editor, create a new file called index.html with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CORS Test - Secondary Bucket</title>
</head>
<body>
    <h1>CORS Test Page</h1>
    <p>This page is hosted on the secondary bucket. It will attempt to fetch data.json from the primary bucket using a cross-origin request.</p>
    <h2>Cross-Origin Fetch Result:</h2>
    <pre id="cors-output">Fetching data from primary bucket...</pre>
    <script>
        const primaryBucketUrl = '[[PRIMARY_DATA_URL="http://your-name-website-lab-2026.s3-website-us-east-1.amazonaws.com/data.json"]]';
        fetch(primaryBucketUrl)
            .then(response => {
                if (!response.ok) {
                    throw new Error('HTTP ' + response.status);
                }
                return response.json();
            })
            .then(data => {
                document.getElementById('cors-output').textContent = 'SUCCESS! Data received:\n' + JSON.stringify(data, null, 2);
            })
            .catch(error => {
                document.getElementById('cors-output').textContent = 'CORS ERROR: ' + error.message + '\nCheck the browser console (F12) for details.';
            });
    </script>
</body>
</html>

What does this page do?

The primaryBucketUrl variable points to the data.json file on your primary bucket's website endpoint. When the page loads, fetch() makes an HTTP GET request to that URL.

Because this page is served from a different origin (the secondary bucket's endpoint), the browser treats this as a cross-origin request. The browser will block the response unless the primary bucket explicitly allows it through CORS headers.

Replace the URL with your primary bucket's endpoint

The primaryBucketUrl value must be your primary bucket's website endpoint followed by /data.json. Copy the endpoint from your primary bucket's Properties > Static website hosting section and append /data.json to it.

  • Save this index.html file to your computer.
  • In the S3 console, navigate to your secondary bucket.
  • Click Upload, then Add files.
  • Select the index.html you just created and click Upload.

Now visit your secondary bucket's website endpoint in a new browser tab. You should see the heading "CORS Test Page" and the text "CORS ERROR" in the output area.

  • Press F12 to open the browser Developer Tools.
  • Click the Console tab.
  • Look for an error message mentioning a CORS policy issue. This confirms that the browser blocked the cross-origin request.

Why does the fetch fail?

Browsers enforce the same-origin policy. When a page from one origin (your secondary bucket) tries to fetch a resource from a different origin (your primary bucket), the browser sends the request but blocks the response unless the target server includes an Access-Control-Allow-Origin header.

Your primary bucket does not include that header yet. You will fix this in the next substep by adding a CORS configuration.

Add a CORS configuration to the primary bucket

CORS configuration tells S3 which external origins are allowed to read responses from your bucket. You will add a rule that permits GET requests from your secondary bucket's website endpoint.

  • In the S3 console, navigate to your primary bucket.
  • Go to the Permissions tab.
  • Scroll down to Cross-origin resource sharing (CORS) and click Edit.
  • Paste the following CORS configuration, replacing the AllowedOrigins URL with your secondary bucket's website endpoint:
[
    {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET"],
        "AllowedOrigins": ["[[SECONDARY_ORIGIN="http://your-name-assets-lab-2026.s3-website-us-east-1.amazonaws.com"]]" ],
        "ExposeHeaders": [],
        "MaxAgeSeconds": 3000
    }
]

What do these CORS fields mean?

  • AllowedHeaders: which request headers the browser can include. The wildcard * allows all headers.
  • AllowedMethods: which HTTP methods are permitted. You only need GET for fetching files.
  • AllowedOrigins: the exact origin URL that is allowed to read responses. This must match your secondary bucket's website endpoint exactly, including the http:// protocol.
  • MaxAgeSeconds: how long the browser caches the CORS preflight response (3000 seconds = 50 minutes).

The origin must match exactly

S3 website endpoints do not support HTTPS. Make sure your AllowedOrigins value starts with http:// and does not include a trailing slash. Copy the endpoint directly from your secondary bucket's Properties > Static website hosting section.

  • Click Save changes.

Your primary bucket now includes CORS headers in its responses to requests from the secondary bucket's origin.

Seeing an error when saving the CORS configuration?

The CORS editor requires valid JSON. Check that you have square brackets wrapping the array, curly braces around the rule object, and commas between each key-value pair. Ensure there are no trailing commas after the last item in each array or object.

help me fix a JSON formatting error in my S3 CORS configuration

Your two-bucket setup is complete. In the next step, you will refresh the secondary bucket's website and confirm the cross-origin fetch succeeds with the CORS headers in place.

Test the Website and Verify CORS

Your two buckets are configured and your CORS rule is in place. The real question is whether the browser actually respects your configuration.

In this step, you will prove that CORS works by watching a successful cross-origin fetch in the browser. You will also intentionally break it to confirm the browser blocks unauthorized origins.

In this step, get ready to:
  • Verify the cross-origin fetch succeeds and inspect the response headers.
  • Test CORS failure by temporarily changing the allowed origin.
  • Verify your custom error page and understand the difference between S3 endpoint types.
Verify the cross-origin fetch succeeds

The secondary bucket's page uses JavaScript to fetch data.json from the primary bucket. If your CORS configuration is correct, this cross-origin request will succeed.

  • Open your secondary bucket's website endpoint in your browser. This is the URL in the format http://your-name-assets-lab-2026.s3-website-us-east-1.amazonaws.com.
  • Look at the page content. You should see "SUCCESS! Data received:" followed by the JSON data from your primary bucket.
  • Press F12 to open Developer Tools.
  • Click the Console tab. Confirm there are no CORS errors listed.
  • Click the Network tab.
  • Refresh the page while the Network tab is open so it captures the requests.
  • Find the request for data.json in the list and click on it.
  • Look at the Response Headers section. You should see an Access-Control-Allow-Origin header with the value of your secondary bucket's website endpoint.

What does the Access-Control-Allow-Origin header mean?

When a browser makes a cross-origin request, it checks the response for this header. If the header's value matches the page's origin, the browser allows the JavaScript to read the response.

Without this header, the browser blocks the response even though the server sent the data successfully. CORS is enforced by the browser, not the server.

Still seeing a CORS error?

Make sure the AllowedOrigins value in your primary bucket's CORS configuration matches your secondary bucket's website endpoint exactly, including http:// and no trailing slash.

Also check that you saved the CORS configuration after editing it. Try a hard refresh (Ctrl+Shift+R) to clear any cached responses.

help me debug my CORS error between two S3 buckets

Test CORS failure by changing the allowed origin

Seeing CORS succeed is only half the picture. To truly understand CORS, you need to confirm the browser blocks requests from unauthorized origins.

  • Go back to the S3 console and open your primary bucket.
  • Click the Permissions tab.
  • Scroll to Cross-origin resource sharing (CORS) and click Edit.

Find this line in your CORS configuration:

"AllowedOrigins": ["http://your-name-assets-lab-2026.s3-website-us-east-1.amazonaws.com"]
  • Replace the origin URL with http://example.com so the line looks like this:
"AllowedOrigins": ["http://example.com"]
  • Click Save changes.
  • Go back to your secondary bucket's website in the browser.
  • Hard refresh the page with Ctrl+Shift+R to bypass any cache.

The page should now display "CORS ERROR" in the output area. Check the Console tab in Developer Tools to see the blocked request.

Why test failure on purpose?

Verifying that unauthorized origins are blocked proves your CORS configuration is actually doing its job. If you only test success, you cannot rule out that S3 is simply allowing all origins regardless of your settings.

  • Now restore the correct origin. Go back to your primary bucket's Permissions tab and edit the CORS configuration.
  • Change http://example.com back to your secondary bucket's website endpoint.
  • Click Save changes.
  • Hard refresh the secondary bucket's website again. Confirm the fetch succeeds and displays "SUCCESS! Data received:" once more.

Fetch still failing after restoring the origin?

Browser caching can hold onto the failed response. Try opening the secondary bucket's URL in an incognito/private window, or clear the browser cache and try again.

help me fix my CORS configuration after restoring the allowed origin

Verify the custom error page

When you enabled static website hosting, you set error.html as the error document. S3 serves this page whenever a visitor navigates to a path that does not match any object in the bucket.

  • In your browser's address bar, navigate to your primary bucket's website endpoint with a non-existent path appended. For example: http://your-name-website-lab-2026.s3-website-us-east-1.amazonaws.com/this-does-not-exist.
  • Confirm you see your custom error page with the heading "404 - Page Not Found" and the link back to the homepage.

Website endpoint vs REST API endpoint

S3 has two types of endpoints for each bucket. The website endpoint (the one you are using) handles requests like a web server. It serves your index.html for root requests and your error.html for missing objects.

The REST API endpoint (the Object URL format you saw earlier, like https://bucket-name.s3.amazonaws.com/key) does not serve custom error pages. It returns raw XML error responses instead. For the SAA-C03 exam, remember that website endpoints are HTTP-only and only support GET and HEAD requests on objects.

Seeing an XML error instead of your error page?

Make sure you are using the website endpoint URL (contains s3-website in the hostname), not the Object URL (REST API endpoint). Only the website endpoint serves custom error documents.

help me understand why I see XML instead of my custom error page on S3

Secret mission

Add Redirect Routing Rules

Real websites change their URL structure all the time. When pages move, visitors hitting old bookmarks get ugly 404 errors. Can you configure S3 to automatically redirect old URLs to their new locations, just like a production site migration?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses only S3 storage well within the AWS Free Tier (5 GB Standard storage, 20,000 GET requests, 2,000 PUT requests per month for 12 months), so there are no ongoing cost concerns.

Re-enable account-level Block Public Access

During this lab you disabled account-level Block Public Access to allow your buckets to serve public content. After finishing, re-enable this setting to restore your account's default security posture.

Resources you used:

  • Primary S3 bucket (your website bucket with index.html, error.html, and data.json).
  • Secondary S3 bucket (your assets bucket with index.html).
  • Account-level Block Public Access settings (modified to allow public access).

Keep everything running

No action needed. Choose this if you are still actively building or plan to use these buckets in future labs in this series.

  • Your two S3 buckets remain accessible via their website endpoints.
  • Storage costs are negligible (a few KB of HTML and JSON files).
  • Your account-level Block Public Access remains disabled. Remember to re-enable it when you no longer need public bucket access.

Pause - I'll come back to this later

Remove public access while keeping your files intact. This prevents anyone from reaching your site while preserving your work.

  • Go to the S3 console at https://console.aws.amazon.com/s3/.
  • Select your primary bucket and go to the Permissions tab.
  • Under Block public access (bucket settings), click Edit and check Block all public access. Save changes.
  • Repeat for your secondary bucket.
  • Re-enable account-level Block Public Access. In the S3 left-hand menu, click Block Public Access settings for this account, click Edit, check Block all public access, type confirm, and click Save changes.

Delete - I don't want to use this again

Remove all project resources and restore your account's default security posture.

  • Go to the S3 console at https://console.aws.amazon.com/s3/.
  • Select your primary bucket by clicking the checkbox next to its name.
  • Click Empty at the top of the page. Type permanently delete in the confirmation field, then click Empty.
  • Once the bucket is empty, select it again and click Delete at the top of the page. Type your bucket name in the confirmation field, then click Delete bucket.
  • Repeat the empty and delete process for your secondary bucket.
  • Re-enable account-level Block Public Access. In the S3 left-hand menu, click Block Public Access settings for this account, click Edit, check Block all public access, type confirm, and click Save changes.

That's a Wrap!

That's a Wrap!

Nice work! You have built a fully functional static website on Amazon S3 and proven you understand the security controls from the inside out.

You've learned how to:

  • Built a static website hosted on S3 with index and error documents, configured public read access through a bucket policy, and understood how all four Block Public Access settings control S3's security model.
  • Created a secondary bucket and configured CORS to allow cross-origin requests, then verified the behavior by testing both success and failure scenarios.
  • Understood S3's object storage model (buckets, objects, keys, metadata, flat namespace), 11 nines durability, and strong read-after-write consistency.
  • Added redirect routing rules to handle URL migrations, simulating a real-world site move.

Ready to quiz yourself?