Course outline

10.3

Static Website Hosting and CORS

Serving a site from a bucket, the 403 trap, and cross-origin requests with preflight.

18 min Reading + animations

Tip: tap or hover any dotted-underlined term to see what it means.

By the end of this lesson you will be able to

  • Explain what a static website is, why S3 can host one without any server, and when it is the cheapest correct answer.
  • Turn a bucket into a website: enable hosting, set the index and error documents, unblock public access and write the public-read bucket policy.
  • Read both website endpoint formats, and compare the website endpoint with the REST endpoint: HTTP only, public reads only, HTML errors, index documents and redirects.
  • Diagnose the 403 trap and every other common error (404, 302, 400, HTTPS failures, SSE-KMS, Requester Pays) from the status code alone.
  • Configure the three kinds of redirect, a custom domain with Route 53, and HTTPS through CloudFront or Amplify Hosting.
  • Explain origins, the same-origin policy and CORS, predict when a browser sends a preflight, and write a CORS configuration that allows exactly what a page needs.

#A website with no server

#What “static” means

A static website is a set of files that are sent to the browser exactly as they are stored: HTML pages, CSS style sheets, JavaScript files, images, fonts, .pdf downloads. Nothing is computed on the server for each visitor. A dynamic site runs code on a server for every request (PHP, Node.js, Python, Java…) to build the page, read a database or check a login.

“Static” does not mean “boring”. A modern single-page application (SPA) built with React, Angular or Vue compiles to static files. All the interactivity runs as JavaScript in the browser, which can call APIs elsewhere (API Gateway, Lambda, an ALB) for dynamic data. The files themselves are static, and S3 can serve them.

What S3 cannot do is run code on the server side: no PHP, no Node.js, no Python, no server-side rendering, no database queries, no form handler. If a page has to be built per request, that part needs EC2, containers or Lambda (chapter 15).

#What it costs

Website hosting is a setting, not a separate product, and has no fee of its own. You pay the normal S3 prices: storage per GB-month, a small charge per GET request, and data transfer out to the internet per GB (lesson 10.1 lists the components). For a busy site, CloudFront in front usually makes it cheaper and faster, because transfer from S3 to CloudFront is free and the edge caches serve repeat visitors.

#Turning a bucket into a website

  1. Enable static website hosting

    In the bucket’s Properties, under Static website hosting, choose Enable and Host a static website. (The other choice, “Redirect requests for an object”, makes the whole bucket a redirect; see “Redirects” below.)

  2. Name the index document, and optionally the error document

    The index document (usually index.html) is required: it is what S3 returns for the root and for any path ending in a slash. The error document (for example 404.html) is optional. Both names are case-sensitive and must match the uploaded objects exactly. Then upload the site’s files.

  3. Turn off Block Public Access

    New buckets have all four Block Public Access settings on (lesson 10.2), and so may your account. Clear them for this bucket, and at the account level if they are on there: S3 applies the most restrictive combination of the two.

  4. Add a bucket policy that allows public reads

    Grant everyone s3:GetObject on the bucket’s objects (note the /*):

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "PublicReadGetObject",
          "Effect": "Allow",
          "Principal": "*",
          "Action": "s3:GetObject",
          "Resource": "arn:aws:s3:::mishti-menu-example/*"
        }
      ]
    }
    

Notice what the policy does not grant: s3:ListBucket. Visitors can read any object whose name they know, but they cannot list the bucket. That is what you want.

The notes put the requirement in one line: the bucket must allow public read access through the bucket policy. Two more details matter today. Since April 2023, new buckets have ACLs disabled (Object Ownership = bucket owner enforced), so the bucket policy is the only way to make objects public, and it covers every object no matter who uploaded it. On an old bucket with ACLs still enabled, objects uploaded by another account are owned by that account and the bucket policy does not apply to them; they need a public-read ACL, or better, switch the bucket to bucket owner enforced.

#The website endpoint

A website bucket answers on a website endpoint, which is different from the normal S3 API (REST) endpoint. Depending on the Region, it has one of two formats:

FormatExampleRegions
s3-website- dash Regionhttp://mishti-menu-example.s3-website-us-east-1.amazonaws.comThe older Regions: us-east-1, us-west-1, us-west-2, eu-west-1, ap-southeast-1, ap-southeast-2, ap-northeast-1, sa-east-1 (and GovCloud US-West)
s3-website dot Regionhttp://mishti-menu-example.s3-website.ap-south-1.amazonaws.comEvery other Region, including all newer ones (ap-south-1, eu-central-1, us-east-2…)

Both start with http://, never https://. The bucket name comes first, so the endpoint is unique worldwide, just like the bucket name. You do not need to memorise which Region uses which separator; the exam only expects you to recognise both shapes as S3 website endpoints, and the console shows the right one under Static website hosting.

One request to the website endpoint

A visitor opens the Mishti Ghor menu site. The browser talks plain HTTP to the bucket’s website endpoint, which turns the path into an object key, checks that the object is public, and answers with HTML.

Scenario

Hosting on, Block Public Access off, a public-read policy: the normal case.

ap-south-1Mumbai
Browserin Dhaka
Website endpointHTTP only
mishti-menu-examplethe bucket
GET /

Bucket settings

Block Public AccessBucket policyIndex documentError document
OffPublic read on /*index.html404.html
Step 1 of 4
1

The visitor types the website endpoint, mishti-menu-example.s3-website.ap-south-1.amazonaws.com, with no path. The browser sends GET / over plain HTTP on port 80.

#Index documents and folders

S3 has no real folders (lesson 10.1), but the website endpoint makes keys with slashes behave like them:

  • / and /menu/ (a trailing slash) return the index document of that “folder”: index.html and menu/index.html. Every folder needs its own index document, with the same name.
  • /menu (no trailing slash): S3 first looks for an object whose key is exactly menu. If there is none but menu/index.html exists, it answers 302 Found and redirects to /menu/. If neither exists, 404.
  • In a versioned bucket, only the newest version of index.html is served.

#Error documents

When something goes wrong, the website endpoint returns an HTML page (the REST endpoint returns XML). With an error document configured, S3 returns your page for every 4xx error, with the real status code; 5xx errors always get S3’s own page. The error document must be readable by the public too, or visitors get S3’s plain page.

#The 403 trap, and other errors

#Why you get 403 Forbidden

The slides give the classic advice: if you get 403 Forbidden, make sure the bucket policy allows public reads. In real life there are several more causes. They all come down to one fact: website visitors are anonymous, so an object is served only if anonymous reads are allowed, and nothing else gets in the way.

CauseWhy it failsFix
Block Public Access on (bucket or account)A public policy cannot be saved, and one saved earlier is ignoredTurn it off at both levels for this bucket’s needs
No bucket policyObjects are private by default; turning off Block Public Access alone makes nothing publicAdd the public-read policy
Policy Resource is arn:aws:s3:::bucket without /*That names the bucket, not its objectsUse arn:aws:s3:::bucket/*
Objects owned by another account (old bucket with ACLs enabled)Bucket policies apply only to objects the bucket owner ownsBucket owner enforced, or a public-read ACL on those objects
Objects encrypted with SSE-KMSDecrypting needs a KMS call by an authorised identity; an anonymous visitor has noneSSE-S3 for public content, or CloudFront with OAC and a key policy (lesson 10.8)
Requester Pays turned onAn anonymous visitor cannot be billed; website endpoints never work with itTurn it off (lesson 10.7)
An explicit Deny in the bucket policy (for example aws:SourceIp)Deny always wins (lesson 2.2)Adjust the condition

#404 and the other status codes

StatusWhen the website endpoint returns it
200 OKThe object (or the index document) exists and is publicly readable
301 Moved PermanentlyA redirect rule, a “redirect all requests” bucket, or an object with redirect metadata
302 FoundA folder was requested without its trailing slash
304 Not ModifiedThe browser’s cached copy is still current (conditional request)
400 Bad RequestThe request reached the wrong Region’s website endpoint for this bucket
403 ForbiddenThe object exists but is not publicly readable (see the table above)
404 Not FoundThe key does not exist, the index document is missing, the bucket does not exist, or the bucket is not configured as a website
405 Method Not AllowedAnything other than GET or HEAD
500 / 503An internal error, or “slow down”: reduce the request rate (lesson 10.6)

There is a side effect worth knowing: because a public website answers 404 for missing keys and 403 for private ones, anyone can tell which keys exist. If that matters, do not enable website hosting; keep the bucket private behind CloudFront.

Try to reproduce each cause yourself. Start with the default settings, as a new bucket has them, and work your way to a 200:

Website tester: why 403?

Set up the bucket, pick an endpoint and a path, and send an anonymous request, the way a visitor’s browser would. Try to reproduce every 403 and 404 in this lesson.

Bucket mishti-menu-example

Objects: index.html, about.html, 404.html, menu/index.html

Bucket policy

Encryption of the objects

Path

Press “Send request” to see the answer.

#Website endpoint or REST endpoint

A website bucket still has its normal REST endpoint, https://mishti-menu-example.s3.ap-south-1.amazonaws.com, and the two behave very differently:

REST endpointWebsite endpoint
Built forPrograms: the S3 API, SDKs, the CLIBrowsers
Access controlPublic and private content (signed requests, IAM)Publicly readable content only
RequestsEvery bucket and object operationGET and HEAD on objects only
GET /Lists the bucket (if allowed)Returns the index document
Folders (/menu/)Just a key; no index documentmenu/index.html
ErrorsXMLHTML, optionally your error document
RedirectsNone (an object with redirect metadata is returned as is)Bucket-level and object-level redirects
HTTPSYesNo, HTTP only
Access points, Requester PaysSupportedNot supported

Same bucket, two endpoints

A bucket with website hosting on answers on two different hostnames. The REST endpoint is the normal S3 API; the website endpoint is a small web server for browsers. Same objects, different behaviour.

Scenario

Browser
Website endpoint…s3-website.ap-south-1…
REST endpoint…s3.ap-south-1…
GET / (HTTP)
Step 1 of 4
1

On the website endpoint, the root means the index document.

Self-checkQuestion

A developer opens https://mishti-menu-example.s3.ap-south-1.amazonaws.com/ in a browser, expecting the home page. The bucket has website hosting and a public-read policy. What does the browser show?

#Redirects

The website endpoint can send visitors elsewhere in three ways:

  1. Redirect all requests to another host. Set a whole bucket’s hosting option to “Redirect requests for an object”, with a host name (and optionally http or https). The classic use is www.example.com → example.com: two buckets named after the two names, the content in one, the other empty and redirecting, with the path kept.
  2. Routing rules in the website configuration (JSON in the console, up to 50 rules). A rule has a condition, a key prefix (KeyPrefixEquals) and/or an error code (HttpErrorCodeReturnedEquals), and a redirect: a new host, protocol, replacement prefix (ReplaceKeyPrefixWith) or replacement key (ReplaceKeyWith), and a redirect code. Use it when a folder is renamed, a section is retired, or 404s should go to a server that can handle them.
  3. Object redirect: set the object’s x-amz-website-redirect-location metadata to another key (with a leading /) or a full URL. The website endpoint answers 301; the REST endpoint ignores it and returns the object. No 50-rule limit, so it suits many individually moved pages.
[
  {
    "Condition": { "KeyPrefixEquals": "docs/" },
    "Redirect": { "ReplaceKeyPrefixWith": "documents/" }
  },
  {
    "Condition": { "HttpErrorCodeReturnedEquals": "404" },
    "Redirect": { "HostName": "app.example.com", "ReplaceKeyPrefixWith": "report-404/" }
  }
]

Three kinds of redirect

A website bucket can redirect everything to another host, redirect by rule (a key prefix or an error code), or redirect one object through its metadata.

Scenario

Browser
www.example.comredirect only
example.comthe content
App serverdynamic pages
Redirect all requests → example.com
Step 1 of 5
1

To serve both example.com and www.example.com, you create two buckets named exactly like the two names. The content lives in example.com; the www bucket is empty and set to “redirect requests for an object” to the host example.com.

#Your own domain, and HTTPS

#A custom domain with Route 53

Visitors should type example.com, not a long S3 hostname. The notes say a custom domain needs CloudFront; that is only half true. A custom domain alone does not need CloudFront:

  • Name the bucket exactly like the domain: example.com (and www.example.com for the www redirect bucket).
  • In Route 53, create an Alias A record for example.com that targets the S3 website endpoint (lesson 8.2). A subdomain can also use a CNAME to the website endpoint at any DNS provider.

This works because S3 picks the bucket from the Host header of the HTTP request. It also means: if the bucket name example.com is taken by someone else, the method is impossible, and the bucket does not appear as an Alias target unless the names match.

#HTTPS needs CloudFront (or Amplify Hosting)

The website endpoint has no HTTPS at all, and you cannot attach a certificate to it. Today browsers label HTTP sites “Not secure”, and many features (service workers, geolocation, the clipboard) need HTTPS. The options:

  • CloudFront with the website endpoint as a custom origin. CloudFront serves HTTPS with a free ACM certificate (requested in us-east-1 for CloudFront) for example.com; the Route 53 Alias points at the distribution. You keep index documents in every folder and the redirects, but the bucket stays public and the CloudFront-to-S3 hop is HTTP.
  • CloudFront with the REST endpoint and Origin Access Control (OAC). Website hosting off, Block Public Access on, the bucket policy allows only that distribution. HTTPS end to end, nobody can bypass CloudFront, and SSE-KMS works. You set a default root object for /, and handle sub-folder index pages and redirects with CloudFront Functions. This is what AWS recommends for most new sites, and it is covered in depth in chapter 11 (CloudFront basics).
  • AWS Amplify Hosting (side note). The S3 documentation now recommends it as the simplest way to put HTTPS on a site stored in S3: you pick the bucket and prefix, and Amplify deploys the files to a CloudFront-backed CDN with an HTTPS URL and custom domains. It is not on the SAA-C03 exam; there the answer to “HTTPS for an S3 static website” is CloudFront.

Your own domain, and HTTPS

Route 53 can point example.com straight at a website bucket over HTTP. For HTTPS, or to keep the bucket private, CloudFront sits in front.

Scenario

ap-south-1
Visitor
Route 53example.com
example.combucket
example.com?
Step 1 of 3
1

A bucket named exactly example.com has website hosting on. In Route 53, an Alias A record for example.com targets that bucket’s website endpoint (lesson 8.2). No CloudFront needed for a custom domain.

#CORS: when the page and the files live apart

#Origins and the same-origin policy

An origin is the triple scheme + host + port of a URL:

  • https://www.example.com has scheme https, host www.example.com and the implied port 443 (80 for http).
  • https://www.example.com/app1 and https://www.example.com/app2 are the same origin: the path does not count.
  • https://www.example.com and https://other.example.com are different origins (different host), as are http://www.example.com (different scheme) and https://www.example.com:8443 (different port).

Browsers follow the same-origin policy: a script loaded from one origin may send requests to another origin, but may not read the response unless the other origin explicitly allows it. Without this rule, any website you visit could use your logged-in session to read your bank’s pages. CORS (Cross-Origin Resource Sharing) is how the other origin says “this origin may read me”: with response headers such as Access-Control-Allow-Origin.

Three points people often miss:

  • CORS is enforced by the browser, not by S3. The same file opens fine in a new tab, with curl, or from a server. S3 only answers the browser’s questions.
  • It applies to cross-origin reads made by scripts (fetch, XMLHttpRequest), to web fonts loaded by CSS, and to images or video used with the crossorigin attribute (for example drawn on a canvas). A plain <img src> from another origin displays without any CORS.
  • CORS never grants permission. The bucket policy, IAM or a signature still decides whether S3 serves the object; CORS only decides whether the browser lets the script see the answer.

#Simple requests and preflights

Browsers sort cross-origin requests into two kinds:

  • Simple requests: GET, HEAD or POST with only “safe” headers and a plain Content-Type (form data or text). The browser sends the request at once, with an Origin header, then checks the response for Access-Control-Allow-Origin. If it is missing or names another origin, the script gets a CORS error even though S3 sent the data.
  • Preflighted requests: any other method (PUT, DELETE), a custom header (Authorization, x-amz-*), or a Content-Type such as application/json. Before sending the real request, the browser sends an OPTIONS request, the preflight, carrying Origin, Access-Control-Request-Method and Access-Control-Request-Headers. Only if the answer allows the origin, the method and every header does the real request go out.

The slides’ picture of the preflight: the browser asks the cross-origin server “may https://www.example.com send GET, PUT, DELETE?”; the answer carries Access-Control-Allow-Origin: https://www.example.com and Access-Control-Allow-Methods: GET, PUT, DELETE; then the real request follows. The Access-Control-Max-Age header lets the browser cache that answer and skip the next preflight (browsers cap it; Chrome at 2 hours).

CORS between a site and a bucket

The page comes from https://www.example.com. Its JavaScript wants data from the bucket assets-example on its S3 REST endpoint, a different origin. The browser enforces the rules; S3 only sends headers that say what it allows.

Scenario

www.example.comorigin of the page
Browserruns the JS
assets-exampleother origin
GET /
Step 1 of 4
1

The browser loads the shop’s page. Its origin is scheme + host + port: https://www.example.com (port 443 implied).

#The CORS configuration on a bucket

You add the configuration to the bucket that serves the resource, the cross-origin target, never to the site that makes the request. In the S3 console it is JSON (Permissions → Cross-origin resource sharing); the API and CLI also accept it (put-bucket-cors), and older documents show the same rules in XML. A configuration holds up to 100 rules, each with:

ElementMeaning
AllowedOrigins (required)Origins allowed, such as https://www.example.com. Each string may contain one * wildcard (https://*.example.com), and * alone allows every origin
AllowedMethods (required)Any of GET, PUT, POST, DELETE, HEAD
AllowedHeadersHeaders the browser may ask to send in a preflight (Access-Control-Request-Headers); one * per string, so x-amz-* or *
ExposeHeadersResponse headers the script is allowed to read, such as ETag (needed for multipart uploads from the browser) or x-amz-meta-* values
MaxAgeSecondsHow long the browser may cache the preflight answer
IDAn optional name for the rule

S3 checks the rules in order and uses the first rule whose origin, method and (for a preflight) every requested header match. If none matches, a preflight gets 403 Forbidden (“This CORS request is not allowed”, or “CORS is not enabled for this bucket” when there is no configuration at all), and the browser never sends the real request.

[
  {
    "ID": "public-read-anywhere",
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": []
  },
  {
    "ID": "admin-uploads",
    "AllowedOrigins": ["https://www.example.com", "https://admin.example.com"],
    "AllowedMethods": ["PUT", "POST", "DELETE"],
    "AllowedHeaders": ["content-type", "x-amz-*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

The slides’ S3 example is a page served from one website bucket (my-bucket-html) that loads coffee.jpg from a second website bucket (my-bucket-assets). The second bucket must allow the first bucket’s website endpoint, http://my-bucket-html.s3-website.us-west-2.amazonaws.com, as an origin, or *. Note the http://: a rule for https:// would not match.

Try every combination, including the rule-order trap and the case where CORS passes but the bucket policy refuses:

CORS preflight simulator

A page’s script calls the bucket assets-example on its S3 REST endpoint. Pick the bucket’s CORS configuration and the request, then send it and watch what the browser and S3 do.

CORS configuration on assets-example

[
 {
  "AllowedOrigins": [
   "https://www.example.com"
  ],
  "AllowedMethods": [
   "GET",
   "HEAD"
  ],
  "AllowedHeaders": []
 }
]

Method

Extra request headers

Press “Send request” to see what happens.

#CORS mistakes that cost an afternoon

  • The rule is on the wrong bucket. It belongs on the bucket the browser is fetching from.
  • The origin differs by a scheme, a port or a www. http:// vs https://, :8443, example.com vs www.example.com are all different origins. A trailing slash in the rule (https://www.example.com/) never matches either.
  • A header is missing from AllowedHeaders. A PUT from the AWS SDK sends content-type and several x-amz-* headers; list them or use *.
  • The script cannot read ETag. Add it to ExposeHeaders.
  • A CDN cached the wrong answer. With CloudFront in front, forward the Origin, Access-Control-Request-Method and Access-Control-Request-Headers headers and include Origin in the cache key (the managed CORS-S3Origin origin request policy does this), and allow the OPTIONS method; otherwise one origin’s cached response, without CORS headers, is served to another. S3 adds Vary: Origin for this reason.
  • Expecting CORS to protect data. It does not: anyone can call S3 without a browser. Protect data with policies; use CORS only to let your own pages read it.

#Static sites in system design

  • The serverless web front end. Static files in S3, served through CloudFront with OAC, HTTPS and a custom domain; dynamic calls from the browser to API Gateway and Lambda, or to an ALB. No web servers to patch, and it scales to any rush. Most of chapter 15 builds on this shape.
  • Split pages from heavy assets. Pages on the main domain, large media in a separate bucket or behind a separate path. With different origins, add CORS on the asset bucket; with one CloudFront distribution and path behaviours (/assets/*), everything is same-origin and CORS is not needed at all.
  • Direct browser uploads. The browser PUTs files straight into S3 with a pre-signed URL (lesson 10.12), so your servers never handle the bytes. This needs a CORS rule for PUT (and ETag exposed for multipart).
  • A maintenance or failover page. A tiny S3 website as the secondary record of a Route 53 failover policy (lesson 8.3) shows “we’ll be right back” when the main site is down.
  • Know the limits. HTTP only, public only, no server-side code, Regional (one copy in one Region unless CloudFront caches it). If you need any of these, put CloudFront in front or choose another service.

#Try it yourself

With the AWS CLI, in a folder that holds index.html, 404.html and the rest of the site:

# 1. Create the bucket and upload the site (content types are guessed from the extensions)
aws s3 mb s3://mishti-menu-example --region ap-south-1
aws s3 sync ./site s3://mishti-menu-example/

# 2. Turn on website hosting with an index and an error document
aws s3 website s3://mishti-menu-example/ --index-document index.html --error-document 404.html

# 3. Allow public reads: unblock public access for this bucket, then add the policy
aws s3api put-public-access-block --bucket mishti-menu-example \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false
aws s3api put-bucket-policy --bucket mishti-menu-example --policy file://public-read.json

# 4. Test it (dot-Region endpoint for ap-south-1)
curl -I http://mishti-menu-example.s3-website.ap-south-1.amazonaws.com/
curl -I http://mishti-menu-example.s3-website.ap-south-1.amazonaws.com/menu       # 302 → /menu/
curl -I http://mishti-menu-example.s3-website.ap-south-1.amazonaws.com/nothing    # 404
curl -I https://mishti-menu-example.s3.ap-south-1.amazonaws.com/                 # REST: 403 (list)

# 5. Routing rules and an object redirect
aws s3api put-bucket-website --bucket mishti-menu-example --website-configuration file://website.json
aws s3api copy-object --bucket mishti-menu-example --copy-source mishti-menu-example/old-menu.html \
  --key old-menu.html --website-redirect-location /menu/ --metadata-directive REPLACE
aws s3api get-bucket-website --bucket mishti-menu-example

Step 3 keeps the two ACL settings on (ACLs are not used) and turns off only the two that block public policies. website.json holds the index, error and routing rules together:

{
  "IndexDocument": { "Suffix": "index.html" },
  "ErrorDocument": { "Key": "404.html" },
  "RoutingRules": [
    { "Condition": { "KeyPrefixEquals": "docs/" }, "Redirect": { "ReplaceKeyPrefixWith": "documents/" } }
  ]
}

CORS on the assets bucket, then a preflight by hand (curl plays the browser and sends the headers a browser would):

aws s3api put-bucket-cors --bucket assets-example --cors-configuration file://cors.json
aws s3api get-bucket-cors --bucket assets-example

curl -i -X OPTIONS https://assets-example.s3.ap-south-1.amazonaws.com/photos/roshogolla.jpg \
  -H "Origin: https://admin.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: content-type"
# 200 with Access-Control-Allow-Origin if a rule matches; 403 if none does

aws s3api delete-bucket-cors --bucket assets-example

For the API the file needs a wrapper: {"CORSRules": [ …the rules… ]}; the console takes the bare list.

#Summary

Key takeaways

  • S3 hosts static sites (HTML, CSS, JavaScript, images, SPAs built with React or Angular) with no server; it cannot run server-side code (PHP, Node.js, Python). Hosting itself is free: you pay storage, requests and transfer out.
  • Setup: enable hosting → index document (required) and error document (optional, 4xx only) → Block Public Access off (bucket and account) → bucket policy allowing public s3:GetObject on arn:aws:s3:::bucket/*.
  • Endpoint: http://bucket.s3-website-region.amazonaws.com (older Regions) or http://bucket.s3-website.region.amazonaws.com (the rest). HTTP only.
  • 403 Forbidden → not publicly readable: Block Public Access, no or wrong policy, objects owned by another account, SSE-KMS, Requester Pays, an explicit Deny. 404 → no such key or missing index document. 302 → folder without a trailing slash.
  • Website endpoint vs REST endpoint: public only vs public and private, GET/HEAD only vs all operations, index documents, HTML vs XML errors, redirects, no HTTPS vs HTTPS.
  • Redirects: a whole bucket to another host (www → apex), routing rules (prefix or error code, up to 50), or per-object x-amz-website-redirect-location.
  • Custom domain: bucket named exactly like the domain + Route 53 Alias. HTTPS: CloudFront (best with a private bucket and OAC), or Amplify Hosting.
  • Origin = scheme + host + port. The browser enforces the same-origin policy; CORS headers from the other origin relax it. Simple requests carry Origin; others are preflighted with OPTIONS.
  • S3 CORS configuration goes on the bucket serving the resource: JSON rules with AllowedOrigins (one *, or * alone), AllowedMethods (GET, PUT, POST, DELETE, HEAD), AllowedHeaders, ExposeHeaders, MaxAgeSeconds; up to 100 rules, first match wins. CORS never grants access.