What problem did I investigate?
How to serve a static website over HTTPS without making the S3 bucket public, and how to diagnose it when CloudFront returns HTTP 403 during configuration.
Cloud Systems: From Architecture to Evidence
A hands-on AWS case study of private object storage, secure origin access, content delivery and HTTPS, with an honest, step-by-step troubleshooting record.
Executive summary
How to serve a static website over HTTPS without making the S3 bucket public, and how to diagnose it when CloudFront returns HTTP 403 during configuration.
Visitor → HTTPS → Amazon CloudFront → Origin Access Control → private S3 REST origin. Route 53 and ACM support the custom-domain and certificate path; website content does not pass through them.
Keep S3 Block Public Access on. Let only one CloudFront distribution read objects (s3:GetObject) through a scoped bucket policy. Serve viewers over HTTPS. Each control is marked verified only when evidence exists; today most are still awaiting evidence.
Intended design and actual configuration are different things. A 403 is a symptom, not a diagnosis: inspect the evidence, form hypotheses, test one at a time, and record the result.
The business problem
Three failure modes are worth designing against. Only one of them (the 403) is something I actually ran into; the other two are general risks the architecture is meant to address.
Architecture
Content flows along the solid cyan path only. Route 53 and ACM (dashed, blue) support the path but never carry website content. Select any component to read what it does.
Select a component in the diagram, or jump directly to one below.
Viewer · not an AWS service
The browser looks up the hostname, opens a TLS connection to a CloudFront edge location and sends an HTTP GET. It only ever talks to CloudFront, never to the bucket.
DNS · optional (custom domain only)
Purpose of the custom domain: a friendly name instead of the generated *.cloudfront.net name. Route 53 holds the DNS record (typically an alias to the distribution) so the name resolves to CloudFront.
It is consulted before the request and carries no website content. Another DNS provider could do the same job.
Certificates · needed for a custom domain
Purpose of TLS: encrypts traffic between visitor and CloudFront and lets the browser verify it reached the right domain. For a custom domain, CloudFront requires the ACM certificate to be in the us-east-1 Region.
On the default *.cloudfront.net domain, CloudFront supplies its own certificate and ACM is not needed.
Content-delivery layer · required
The only public entry point. It terminates viewer HTTPS, serves cached copies from edge locations, and on a cache miss requests the object from the origin. It returns the configured default root object (for example index.html) when the viewer requests /.
Origin-access control · required for a private S3 REST origin
CloudFront signs each origin request (SigV4). The bucket policy then allows the CloudFront service principal to perform s3:GetObject, restricted to one distribution by an AWS:SourceArn condition.
OAC works with an S3 REST origin. It does not apply to the S3 website endpoint.
Private storage layer · required
Stores index.html, CSS and images. Block Public Access stays on and the bucket policy grants read access only to the intended distribution, so direct requests to the bucket URL should be denied.
Intended, not yet evidenced. See Security.
s3:GetObject.Architecture decisions and trade-offs
s3:GetObject for one named distribution, with no public read needed.us-east-1 and matching alternate domain names.Security review
Security depends on the actual configuration, not on the diagram. Every status below defaults to Needs verification unless there is evidence I can point to.
| Control | Status | What is known | Evidence needed to mark Verified |
|---|---|---|---|
| S3 Block Public Access | Needs verification | My study notes (E1) say it was kept enabled. | Console screenshot or get-public-access-block output showing all four settings on (account IDs redacted). |
| S3 bucket policy | Needs verification | Notes (E1) say a policy allowing the distribution to read objects was added. The policy text was not preserved. | Redacted policy JSON: only s3:GetObject, principal cloudfront.amazonaws.com, AWS:SourceArn set to the one distribution. |
| CloudFront OAC | Needs verification | Notes (E1) say OAC was configured. | Distribution origin settings showing an OAC attached with “Sign requests”. |
| Correct origin type | Needs verification | Intended: S3 REST endpoint. Not confirmed from evidence. | Origin domain of the form <bucket>.s3.<region>.amazonaws.com, not an s3-website endpoint. |
| Default root object | Needs verification | Not recorded in notes. | Setting shows index.html; / returns the page. |
| HTTPS | Needs verification | Notes (E1) say the site loaded over the CloudFront HTTPS URL. | Response headers from the HTTPS URL; viewer protocol policy set to redirect HTTP to HTTPS. |
| Certificate validity | Needs verification | The default CloudFront certificate was used per notes (E1). No custom certificate is claimed. | Browser certificate details; for a custom domain, an ACM certificate in us-east-1 with status Issued. |
| Custom-domain configuration | Planned | Notes (E1) describe the CloudFront domain only. Route 53 and ACM are the planned path. | Alternate domain name on the distribution, ACM certificate, DNS alias record, and a working HTTPS request on the domain. |
| Exposure of secrets in this site | Verified | Scan of this site’s own files for keys, tokens, account IDs and private identifiers (E2). | E2: pattern search of the files in this folder, result recorded in the review report. Scope is this website only, not the AWS account. |
| Direct origin access blocked | Needs verification | Expected by design; never tested. | An unauthenticated request to an object’s S3 URL returns an access-denied response (record the actual response). |
Before publishing screenshots of the AWS console, redact the 12-digit account ID, distribution ID, bucket ARN and any access keys.
Troubleshooting: CloudFront 403
CloudFront returned a 403 error during project configuration. The root cause is not yet confirmed, so this section is the investigation method and an incomplete incident record, not a success story.
CloudFront returned HTTP 403 while configuring the project.
Exact URL, response body, response headers, and what was tried before the site loaded.
Unknown No cause is asserted on this page.
Confirm the distribution shows Deployed, not In progress.
Why: a distribution still deploying can behave inconsistently.
Observed: not recorded
Read the origin domain name exactly as configured.
Why: it identifies which kind of S3 endpoint CloudFront is calling.
Observed: not recorded
REST: <bucket>.s3.<region>.amazonaws.com. Website: contains s3-website.
Why: OAC supports an S3 REST origin, not the website endpoint. This design needs REST.
Observed: not recorded
index.html exists at the expected keyList the bucket and check the key matches exactly, including case and any folder prefix.
Why: an object in a sub-folder or with a different name is not found at /index.html.
Observed: not recorded
It should be index.html, with no leading slash.
Why: without it, a request for / asks S3 for the bucket itself rather than a page, which can be denied.
Observed: not recorded
On an S3 REST origin, check that an OAC is attached and signing behavior is set to sign requests.
Why: unsigned requests cannot satisfy a policy that only trusts CloudFront.
Observed: not recorded
Read the statement text, bucket name and distribution reference in full.
Why: a typo or a stale distribution reference denies access even if everything else is right.
Observed: not recorded
s3:GetObject?Compare against this template. Replace placeholders with your own values; never publish real IDs.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<BUCKET_NAME>/*",
"Condition": { "StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DISTRIBUTION_ID>"
} }
}]
}
Why: the condition scopes read access to one distribution, which is the least-privilege model.
Observed: not recorded
Confirm the domain is listed as an alternate domain name and that a valid ACM certificate in us-east-1 covers it. Compare with the default CloudFront URL.
Why: if the CloudFront URL works but the custom domain does not, the fault is in domain, certificate or DNS, not the origin.
Observed: not recorded (applies only if a custom domain is used)
Change one thing, tied to one hypothesis that the evidence supports. Write down what and why.
Why: several simultaneous changes make the real cause impossible to identify.
Change made: not recorded
Request the same URL again, allowing for cached error responses and deployment time.
Why: a fix is only confirmed by a repeatable test.
Result: not recorded
Write down the exact response, the evidence, and the cause only if the retest supports it. If it does not, return to step 02 with the new evidence.
Why: the record is what makes the investigation repeatable and explainable.
Confirmed root cause: not recorded
| Hypothesis | Check | Status |
|---|---|---|
| Origin is the S3 website endpoint | Origin domain contains s3-website | Untested |
| OAC not attached or not signing | Origin access settings on the distribution | Untested |
| Bucket policy does not match the distribution | Policy text and AWS:SourceArn value | Untested |
| Object or root object missing | Bucket listing; default root object setting | Untested |
| Custom domain or certificate mismatch | Alternate domain names; ACM certificate; compare with the CloudFront URL | Untested |
To complete it, edit the fields in index.html once you have real evidence. Placeholders are marked “To complete”.
Verification matrix
Each test has an expected result. “Actual” stays blank until I run it and attach evidence. Replace <site-url> with your CloudFront or custom-domain URL.
| ID | Objective | Procedure | Expected | Actual | Status | Evidence |
|---|---|---|---|---|---|---|
| T01 | Distribution is deployed | Open the distribution in the CloudFront console. | Status is Deployed. | Not run | Pending | None |
| T02 | HTTPS response | curl -I https://<site-url>/ | Successful response over HTTPS. | Not run | Pending | None |
| T03 | HTTP is redirected | curl -I http://<site-url>/ | Redirect to the HTTPS URL. | Not run | Pending | None |
| T04 | Root object served | Open https://<site-url>/ in a browser. | index.html content renders. | Not run | Pending | None |
| T05 | Expected assets load | Browser developer tools, Network tab: reload the page. | CSS, script and images load without errors. | Not run | Pending | None |
| T06 | Direct S3 object access is denied | Request an object using its S3 REST URL, unauthenticated. | Access denied; no object returned. | Not run | Pending | None |
| T07 | CloudFront can read the origin via OAC | Request an object through CloudFront; check OAC and policy settings. | Object returned through CloudFront only. | Not run | Pending | None |
| T08 | Certificate and custom domain | Inspect certificate in the browser; check DNS record. Skip if no custom domain. | Valid certificate for the domain; DNS resolves to CloudFront. | Not run | Planned | None |
| T09 | Mobile responsiveness | Load the deployed site at about 360 px width. | No horizontal page scroll; readable text. | Not run on the deployed site | Pending | None |
| T10 | Keyboard accessibility | Tab through the deployed page; open the mobile menu and diagram components. | Visible focus; every control operable by keyboard. | Not run on the deployed site | Pending | None |
Lessons learned
These come from the concepts in my study notes. Lessons that depend on the 403 outcome stay provisional until the root cause is confirmed.
CloudFront reaching S3 is not automatic. Decision: use an S3 REST origin with OAC rather than a public website endpoint.
“Allow CloudFront” is too broad. Decision: grant only s3:GetObject, scoped to one distribution with AWS:SourceArn.
The certificate, the alternate domain name and the DNS record can each fail independently. Decision: test the default CloudFront URL first, then the custom domain.
Deployment and caching mean a change may not show immediately. Decision: check Deployed status and cache behavior before concluding a fix failed.
Changing settings at random hides the real cause. Decision: collect the response, form one hypothesis, change one thing, retest.
A diagram describes intent; only a test shows behavior. Decision: label each control Verified only when evidence is attached.
Next improvements
us-east-1, alternate domain name, HTTPS test.