Back to articles
AWS / DevOpsJuly 1, 20262 min read

Hosting Secure Static Sites on AWS for $0/Month (S3, CloudFront, & OAC)

Architect a secure, production-grade static site hosting stack on Amazon Web Services using private S3 storage, CloudFront CDN, Origin Access Control, and ACM SSL certificates.

Deploying static websites (such as React, Vue, or Vite SPAs) shouldn't be expensive or insecure. While AWS S3 supports direct static website hosting, exposing your bucket publicly is a security vulnerability.

Here is the modern, enterprise-grade architecture to host static sites on AWS securely and for free under normal traffic loads.

1. Private S3 Bucket (Asset Storage)

The foundation is storing build artifacts (the dist folder) in a private S3 bucket. Keep the Block all public access setting enabled. This protects your source files and assets from direct public discovery or hotlinking.
json read-only
// S3 Bucket Policy (Auto-managed via CloudFront OAC principal)
{
  "Version": "2012-10-17",
  "Statement": {
    "Sid": "AllowCloudFrontServicePrincipalReadOnly",
    "Effect": "Allow",
    "Principal": {
      "ServicePrincipal": "cloudfront.amazonaws.com"
    },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::your-bucket-name/*",
    "Condition": {
      "StringEquals": {
        "AWS:SourceArn": "arn:aws:cloudfront::your-account-id:distribution/your-dist-id"
      }
    }
  }
}

2. CloudFront CDN & Origin Access Control (OAC)

Instead of accessing S3 directly, users load pages through Amazon CloudFront's global edge cache locations. By setting up Origin Access Control (OAC), CloudFront authenticates its requests using a service principal before calling S3. This ensures that assets are only accessible through your CDN domain name.
  • Security Benefit: Shields your bucket from unauthorized direct requests.
  • Performance Benefit: Dramatically reduces loading latency by caching files close to your users.
  • Cost Benefit: The CloudFront permanent free tier includes 1 TB of free data transfer out every month.
  • 3. Resolving SPA Routing (React Router / Next.js)

    In a Single Page Application (SPA), the client-side router (like React Router) handles page transitions dynamically. However, if a user refreshes the page on a sub-route (e.g., yoursite.com/projects), CloudFront checks S3 for a directory key named /projects which doesn't exist, returning a 403 Forbidden or 404 Not Found error.

    To resolve this, configure Custom Error Responses in CloudFront: 1. Map 403: Forbidden errors to return /index.html with a 200: OK response code. 2. Map 404: Not Found errors to return /index.html with a 200: OK response code.

    This delegates routing back to the client-side index bundle smoothly without throwing browser errors.

    P
    Pratik Sangani
    Backend Developer & Architect
    Get in touch