Cloud Systems: From Architecture to Evidence

Secure by Design: Hosting a Static Website on AWS

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.

Problem
Publish a website without exposing the storage origin or breaking access with misconfigured origin, DNS or certificate settings.
Services
Amazon S3 · Amazon CloudFront · Origin Access Control. Custom-domain path: Route 53 + ACM.
My part
I performed the lab steps recorded in my study notes and I am documenting the CloudFront 403 investigation.
Status
Lab performed · evidence pending
The 403 root cause is not yet confirmed.
Request path summary A visitor sends an HTTPS request to CloudFront. CloudFront sends a signed request using Origin Access Control to a private S3 bucket. Route 53 provides DNS for the visitor's lookup and ACM provides the certificate used by CloudFront; neither carries website content. Route 53DNS ACMTLS certificate VisitorBrowser CloudFrontCDN · HTTPS S3 bucketPrivate HTTPS OAC Solid = content path · dashed = supporting service
Summary of the intended design. The full diagram is in Architecture.

Executive summary

The project in four answers.

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.

What architecture did I use?

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.

Which security decisions matter?

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.

What did I learn?

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

A small business wants a website that stays up and stays closed.

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.

General risk

A. Incorrect public storage permissions

Technical issue
A bucket made public (policy, ACL or disabled Block Public Access) so anyone can fetch objects directly from S3, bypassing CloudFront.
Consequence
Unintended data exposure; CloudFront controls such as HTTPS enforcement can be bypassed.
Design principle
Least privilege: private bucket, access only for one CloudFront distribution.
Verification
Check Block Public Access settings and the bucket policy, then request an object from the S3 URL directly and expect it to be denied.
Encountered: 403 (cause unconfirmed)

B. Origin configuration that blocks CloudFront

Technical issue
Wrong origin type, missing OAC, a bucket policy that does not authorize the distribution, or a missing object or root object.
Consequence
Visitors see an error instead of the site, even though files exist in S3.
Design principle
Use an S3 REST origin with OAC, and match the bucket policy to the exact distribution.
Verification
Follow the diagnostic flow; retest the affected URL after each justified change.
General risk

C. DNS, domain or certificate mismatch

Technical issue
A custom domain that is not an alternate domain name on the distribution, a missing or unvalidated ACM certificate, or DNS pointing at the wrong target.
Consequence
Browser certificate warnings or errors on the custom domain while the default CloudFront URL still works.
Design principle
Treat DNS and certificates as separate controls with their own checks.
Verification
Compare behavior on the CloudFront domain and the custom domain; inspect the certificate and DNS record.

Architecture

How one request reaches a private bucket.

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.

Architecture diagram: browser to private S3 through CloudFront Left to right: a website visitor sends an HTTPS viewer request to Amazon CloudFront. CloudFront sends a signed origin request, authorized by Origin Access Control, to a private Amazon S3 REST origin. Above the visitor, Amazon Route 53 provides DNS for the custom domain. Above CloudFront, AWS Certificate Manager supplies the certificate for HTTPS. Route 53 and ACM are not in the content path. SUPPORTING · OPTIONALAmazon Route 53DNS for the custom domain SUPPORTING · CUSTOM DOMAINAWS Certificate ManagerTLS certificate DNS lookup (name to target) certificate used for viewer HTTPS VIEWERWebsite visitorBrowser CONTENT-DELIVERY LAYERAmazon CloudFrontEdge delivery · caching PRIVATE STORAGE LAYERAmazon S3 bucketREST origin · not public HTTPS viewer request signed origin request (SigV4) OAC Responses return along the same path in reverse. Route 53 answers DNS queries; ACM supplies a certificate. Neither handles website content. Architecture diagram: browser to private S3 through CloudFront Top to bottom: a website visitor sends an HTTPS viewer request to Amazon CloudFront. CloudFront sends a signed origin request, authorized by Origin Access Control, to a private Amazon S3 REST origin. Beside the path, Amazon Route 53 provides DNS for the custom domain and AWS Certificate Manager supplies the certificate for CloudFront. Neither handles website content. Website visitorBrowser Route 53DNS DNS lookup HTTPS viewer request Amazon CloudFrontEdge delivery · caching ACMTLS certificate certificate for viewer HTTPS OAC signed origin request (SigV4) Amazon S3 bucketREST originnot public Private storage layer. Responses return in reverse.
Route 53 and ACM are drawn beside the path on purpose: Route 53 resolves the domain name before the request, and ACM provides the certificate that CloudFront presents. Neither forwards website content.
  • Website content path
  • Supporting service or control relationship

Select a component in the diagram, or jump directly to one below.

Website visitor

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.

Amazon Route 53

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.

AWS Certificate Manager (ACM)

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.

Amazon CloudFront

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 (OAC)

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.

Amazon S3 (private REST origin)

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.

The same flow as text

  1. DNS (custom domain only): the browser resolves the domain through Route 53; the answer points to CloudFront.
  2. Viewer request: the browser connects to CloudFront over HTTPS, using the certificate CloudFront holds (ACM for a custom domain, the default certificate otherwise).
  3. Cache check: CloudFront serves a cached object if it has one.
  4. Origin request: on a miss, CloudFront sends a request signed with OAC to the S3 REST endpoint.
  5. Authorization: S3 evaluates the bucket policy; only the named distribution may s3:GetObject.
  6. Response: the object returns to CloudFront, which caches it and returns it to the visitor.

Architecture decisions and trade-offs

Five decisions, each with its cost.

Why CloudFront?

Reason
It gives the site a public entry point that is separate from the origin, and it is where HTTPS to the viewer is handled.
Benefit
Edge delivery and caching can reduce load on the origin and bring content closer to visitors. No speed improvement is claimed here because I have not benchmarked one.
Limitation
Caching means changes may not appear until cache entries expire or are invalidated. It also adds a service to configure, and a place for 403 errors to originate.

Why a private S3 bucket?

Reason
Object storage should not be public unless there is a reason. A private bucket makes CloudFront the only intended way in.
Benefit
A smaller exposed surface and one place to enforce HTTPS and caching rules.
Limitation
It is only private if the actual settings are correct. A wrong policy or a disabled Block Public Access setting silently removes the benefit. Hence the evidence matrix.

Why Origin Access Control?

Reason
OAC lets CloudFront read a private S3 REST origin using signed requests and a bucket policy scoped to one distribution. It supersedes the older Origin Access Identity.
Benefit
Least privilege: read-only s3:GetObject for one named distribution, with no public read needed.
Limitation
It requires an S3 REST origin; the S3 website endpoint is not supported. A mismatched policy or origin type is a plausible source of 403 responses.

Why HTTPS?

Reason
It encrypts data in transit and lets the browser validate the certificate for the domain it is visiting.
Benefit
Visitors are protected from passive interception and tampering on the network path.
Limitation
Protects the viewer-to-CloudFront connection. It does not prove the site content is correct or the bucket configuration is safe. Custom domains need a valid ACM certificate in us-east-1 and matching alternate domain names.

Why a static website?

Reason
The content is files that do not change per visitor, so no server-side processing is needed.
Benefit
Low operational complexity: nothing to patch or scale, and a small surface to explain and secure.
Limitation
This design provides no backend application functionality, no user authentication and no database operations. Needing any of those means a different architecture.

Security review

Intended controls are not verified controls.

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.

  • Verified evidence exists and is referenced
  • Needs verification described or reported, no evidence attached
  • Planned not done yet
  • Not applicable not part of this design
Security controls and their evidence status
ControlStatusWhat is knownEvidence needed to mark Verified
S3 Block Public AccessNeeds verificationMy 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 policyNeeds verificationNotes (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 OACNeeds verificationNotes (E1) say OAC was configured.Distribution origin settings showing an OAC attached with “Sign requests”.
Correct origin typeNeeds verificationIntended: S3 REST endpoint. Not confirmed from evidence.Origin domain of the form <bucket>.s3.<region>.amazonaws.com, not an s3-website endpoint.
Default root objectNeeds verificationNot recorded in notes.Setting shows index.html; / returns the page.
HTTPSNeeds verificationNotes (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 validityNeeds verificationThe 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 configurationPlannedNotes (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 siteVerifiedScan 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 blockedNeeds verificationExpected by design; never tested.An unauthenticated request to an object’s S3 URL returns an access-denied response (record the actual response).
Handwritten-style study notes titled 'Hosted a Secure Static Website on AWS'. They list nine steps performed, a four-item validation checklist, a request-flow sketch and key concepts learned.
E1 · Study notes (self-authored). My own summary of the lab: steps performed, a validation checklist and concepts learned. It is a record of what I did, not independent proof of the live configuration.

Before publishing screenshots of the AWS console, redact the 12-digit account ID, distribution ID, bucket ARN and any access keys.

Troubleshooting: CloudFront 403

A 403 is a symptom. This is how I investigate it.

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.

Known symptom

CloudFront returned HTTP 403 while configuring the project.

Not recorded

Exact URL, response body, response headers, and what was tried before the site loaded.

Confirmed root cause

Unknown No cause is asserted on this page.

Why the evidence matters. Different faults can produce a similar 403: a wrong origin type, a missing OAC, a bucket policy that does not name the distribution, a missing object or root object, a custom domain that is not an alternate domain name. The exact response and logs decide which applies. I will not change settings until a specific hypothesis points to them.

Diagnostic flow

  1. Check distribution deployment status

    Confirm the distribution shows Deployed, not In progress.

    Why: a distribution still deploying can behave inconsistently.

    Observed: not recorded

  2. Inspect the origin domain

    Read the origin domain name exactly as configured.

    Why: it identifies which kind of S3 endpoint CloudFront is calling.

    Observed: not recorded

  3. Is it an S3 REST endpoint or an S3 website endpoint?

    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

  4. Confirm index.html exists at the expected key

    List 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

  5. Check the default root object

    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

  6. Inspect OAC attachment and signing

    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

  7. Inspect the S3 bucket policy

    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

  8. Does the policy authorize the right distribution to 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

  9. If it fails on the custom domain: check alternate domain and certificate

    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)

  10. Apply only a justified change

    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

  11. Retest the affected URL

    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

  12. Record the result and the confirmed root cause

    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

Hypotheses to test (none confirmed)

Possible causes of a 403 on this design
HypothesisCheckStatus
Origin is the S3 website endpointOrigin domain contains s3-websiteUntested
OAC not attached or not signingOrigin access settings on the distributionUntested
Bucket policy does not match the distributionPolicy text and AWS:SourceArn valueUntested
Object or root object missingBucket listing; default root object settingUntested
Custom domain or certificate mismatchAlternate domain names; ACM certificate; compare with the CloudFront URLUntested

Incident record Incomplete

To complete it, edit the fields in index.html once you have real evidence. Placeholders are marked “To complete”.

Symptom
CloudFront returned HTTP 403 during project configuration.To complete: exact URL, timestamp, response text.
Investigation steps
Planned flow: the 12 steps above.To complete: which steps were actually performed, in what order.
Evidence collected
None attachedTo complete: redacted screenshots or command output, referenced as E3, E4…
Confirmed root cause
UnconfirmedTo complete: only after a hypothesis is tested and the retest passes.
Corrective action
To complete: the single change made, and why the evidence justified it.
Retest result
To complete: URL, observed response, date.
Lesson learned
To complete after the root cause is confirmed.

Verification matrix

Ten tests. No results invented.

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.

Verification test cases
IDObjectiveProcedureExpectedActualStatusEvidence
T01Distribution is deployedOpen the distribution in the CloudFront console.Status is Deployed.Not runPendingNone
T02HTTPS responsecurl -I https://<site-url>/Successful response over HTTPS.Not runPendingNone
T03HTTP is redirectedcurl -I http://<site-url>/Redirect to the HTTPS URL.Not runPendingNone
T04Root object servedOpen https://<site-url>/ in a browser.index.html content renders.Not runPendingNone
T05Expected assets loadBrowser developer tools, Network tab: reload the page.CSS, script and images load without errors.Not runPendingNone
T06Direct S3 object access is deniedRequest an object using its S3 REST URL, unauthenticated.Access denied; no object returned.Not runPendingNone
T07CloudFront can read the origin via OACRequest an object through CloudFront; check OAC and policy settings.Object returned through CloudFront only.Not runPendingNone
T08Certificate and custom domainInspect certificate in the browser; check DNS record. Skip if no custom domain.Valid certificate for the domain; DNS resolves to CloudFront.Not runPlannedNone
T09Mobile responsivenessLoad the deployed site at about 360 px width.No horizontal page scroll; readable text.Not run on the deployed sitePendingNone
T10Keyboard accessibilityTab through the deployed page; open the mobile menu and diagram components.Visible focus; every control operable by keyboard.Not run on the deployed sitePendingNone

Lessons learned

Six lessons, each tied to a decision.

These come from the concepts in my study notes. Lessons that depend on the 403 outcome stay provisional until the root cause is confirmed.

  1. Origin access is its own design problem

    CloudFront reaching S3 is not automatic. Decision: use an S3 REST origin with OAC rather than a public website endpoint.

  2. Least privilege is specific

    “Allow CloudFront” is too broad. Decision: grant only s3:GetObject, scoped to one distribution with AWS:SourceArn.

  3. HTTPS and DNS are separate layers

    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.

  4. CloudFront changes take time

    Deployment and caching mean a change may not show immediately. Decision: check Deployed status and cache behavior before concluding a fix failed.

  5. Troubleshoot with evidence

    Changing settings at random hides the real cause. Decision: collect the response, form one hypothesis, change one thing, retest.

  6. Intended is not verified

    A diagram describes intent; only a test shows behavior. Decision: label each control Verified only when evidence is attached.

Next improvements

Planned work, not finished work.

  • PlannedComplete the 403 incident recordReproduce or review the symptom, collect evidence, confirm the root cause.
  • PlannedComplete the verification matrixRun T01 to T10 and attach redacted evidence.
  • PlannedCustom domain verificationRoute 53 record, ACM certificate in us-east-1, alternate domain name, HTTPS test.
  • PlannedLogging and monitoringChoose suitable CloudFront and S3 logging; document what each answers.
  • PlannedCache behavior and invalidationDocument cache settings and when an invalidation is needed after updates.
  • PlannedCost reviewEstimate and document the cost drivers. No savings are claimed.
  • PlannedEvaluate AWS WAF only if justifiedAdd it only if a threat model shows a need; it is not part of the current design.
  • PlannedOperational documentationA short runbook for deploying and rolling back site updates.