Skip to content

S3 Hosting Setup Guide

This document details how the menotime-playbook site is hosted as a private, authenticated static site on AWS using S3, CloudFront, and GitHub Actions.

Architecture

The deployment pipeline follows this flow:

Private GitHub Repo → GitHub Actions Build → S3 Bucket → CloudFront (with Auth) → Team
Component Purpose
S3 Stores the built static site files (HTML, CSS, JS). Bucket is fully private with no public access.
CloudFront CDN that serves the site, handles HTTPS, and enforces access control via WAF.
GitHub Actions Builds the static site from markdown and deploys to S3 on every push to main.
IAM OIDC Allows GitHub Actions to authenticate with AWS using short-lived tokens instead of stored credentials.
WAF IP-based access restriction so only your team/VPN can view the site.

Prerequisites

  • An AWS account with admin access
  • AWS CLI installed and configured locally
  • Admin or owner access to the TimelessBiotech GitHub organization
  • The menotime-playbook repository with markdown content
  • A static site generator installed (MkDocs recommended)

Step 1: Create the S3 Bucket

Create a private S3 bucket in us-west-1 (N. California) to store the built site. Block all public access since CloudFront will serve the content.

aws s3 mb s3://menotime-playbook-site --region us-west-1

aws s3api put-public-access-block \
  --bucket menotime-playbook-site \
  --public-access-block-configuration \
  '{"BlockPublicAcls":true,"IgnorePublicAcls":true,"BlockPublicPolicy":true,"RestrictPublicBuckets":true}'

Step 2: Create a CloudFront Distribution

2a. Create an Origin Access Control (OAC)

OAC allows CloudFront to access the private S3 bucket securely.

aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "menotime-oac",
    "SigningProtocol": "sigv4",
    "SigningBehavior": "always",
    "OriginAccessControlOriginType": "s3"
  }'

Note the Id from the output. You will need it in the next step.

2b. Create the CloudFront Distribution

Create a distribution-config.json file with the following contents, replacing <OAC_ID> with the value from the previous step:

{
  "CallerReference": "menotime-playbook-dist",
  "Comment": "menotime-playbook private site",
  "DefaultCacheBehavior": {
    "TargetOriginId": "menotime-s3",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": { "Quantity": 2, "Items": ["GET","HEAD"] },
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "Compress": true
  },
  "Origins": {
    "Quantity": 1,
    "Items": [{
      "Id": "menotime-s3",
      "DomainName": "menotime-playbook-site.s3.us-west-1.amazonaws.com",
      "OriginAccessControlId": "<OAC_ID>",
      "S3OriginConfig": { "OriginAccessIdentity": "" }
    }]
  },
  "DefaultRootObject": "index.html",
  "Enabled": true
}
aws cloudfront create-distribution \
  --distribution-config file://distribution-config.json

Note the Distribution Id and Domain Name from the output.

2c. Add S3 Bucket Policy

Grant the CloudFront distribution permission to read from the bucket:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowCloudFront",
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::menotime-playbook-site/*",
    "Condition": {
      "StringEquals": {
        "AWS:SourceArn": "arn:aws:cloudfront::398673484771:distribution/E3INI5YRMUS5XY"
      }
    }
  }]
}
aws s3api put-bucket-policy \
  --bucket menotime-playbook-site \
  --policy file://bucket-policy.json

Step 3: Restrict Access with AWS WAF

Create a WAF Web ACL to restrict access by IP address. This ensures only your team (e.g., office or VPN IPs) can view the site.

3a. Create an IP Set

Replace the IP ranges below with your team or VPN addresses:

aws wafv2 create-ip-set \
  --name menotime-allowed-ips \
  --scope CLOUDFRONT \
  --region us-east-1 \
  --ip-address-version IPV4 \
  --addresses "104.10.197.7/32" "73.189.156.221/32"

Important: WAF for CloudFront must be created in us-east-1, regardless of where your S3 bucket lives.

3b. Create a Web ACL

Create a Web ACL that blocks all traffic except from your IP set, then associate it with your CloudFront distribution in the AWS Console under CloudFront > Distribution > WAF.

Step 4: Set Up GitHub OIDC Authentication

Instead of storing AWS access keys in GitHub, use OIDC federation for secure, short-lived credentials.

4a. Create an OIDC Identity Provider in AWS

aws iam create-open-id-connect-provider \
  --url "https://token.actions.githubusercontent.com" \
  --client-id-list "sts.amazonaws.com" \
  --thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1"

4b. Create an IAM Role for GitHub Actions

Create a trust policy (trust-policy.json) that only allows the menotime-playbook repo to assume the role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::398673484771:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:TimelessBiotech/menotime-playbook:ref:refs/heads/main"
      }
    }
  }]
}
aws iam create-role \
  --role-name GitHubActionsDeployRole \
  --assume-role-policy-document file://trust-policy.json

4c. Attach Permissions to the Role

Create a policy (deploy-policy.json) granting access to S3 and CloudFront:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::menotime-playbook-site",
        "arn:aws:s3:::menotime-playbook-site/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::398673484771:distribution/E3INI5YRMUS5XY"
    }
  ]
}
aws iam put-role-policy \
  --role-name GitHubActionsDeployRole \
  --policy-name DeployToS3 \
  --policy-document file://deploy-policy.json

Step 5: Create the GitHub Actions Workflow

Add this workflow file to your repository at .github/workflows/deploy.yml:

name: Deploy Playbook to S3
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install MkDocs
        run: pip install mkdocs mkdocs-material

      - name: Build site
        run: mkdocs build

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::398673484771:role/GitHubActionsDeployRole
          aws-region: us-west-1

      - name: Sync to S3
        run: aws s3 sync ./site s3://menotime-playbook-site --delete

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id E3INI5YRMUS5XY \
            --paths '/*'

Step 6: Verify the Deployment

  1. Push a change to the main branch of TimelessBiotech/menotime-playbook.
  2. Go to the Actions tab in GitHub to confirm the workflow runs successfully.
  3. Open https://menotime-playbook.timelessbiotech.com in a browser.
  4. Verify the playbook site loads correctly.
  5. Test from an IP outside your WAF allowlist to confirm access is blocked.

Estimated Monthly Cost

For a small static documentation site with low traffic, this setup is very affordable:

Service Est. Cost Notes
S3 Storage < $0.10 A few MB of HTML/CSS
CloudFront < $0.50 Low traffic, free tier eligible
WAF Web ACL ~$5.00 $5/ACL + $1/rule
GitHub Actions Free 2,000 min/mo on free plan
Total ~$6/month

Note: If you skip WAF and use Lambda@Edge for authentication instead, the base cost drops but varies with request volume. For IP-based restriction, WAF is the simplest option.

CloudFront Function: URL Rewriting

A CloudFront Function named menotime-url-rewrite is attached to the distribution. It automatically appends index.html to directory-style URLs (e.g., /infrastructure/aws-architecture/ becomes /infrastructure/aws-architecture/index.html). Without this function, S3 returns Access Denied for directory URLs since it does not natively resolve index documents through CloudFront.

  • Function ARN: arn:aws:cloudfront::398673484771:function/menotime-url-rewrite
  • Runtime: cloudfront-js-1.0

Custom Domain

The playbook is accessible at https://menotime-playbook.timelessbiotech.com.

  • SSL Certificate ARN: arn:aws:acm:us-east-1:398673484771:certificate/27063a57-bdd0-48f0-b39a-ddf98d149a1f
  • DNS: CNAME record in GoDaddy pointing menotime-playbook.timelessbiotech.com to dpd3q7c2751s3.cloudfront.net

WAF IP Restriction

A WAF Web ACL (menotime-playbook-acl) is associated with the CloudFront distribution. It blocks all traffic by default and only allows requests from the following IP addresses:

  • 104.10.197.7/32
  • 73.189.156.221/32

To add or remove IPs, update the menotime-allowed-ips IP set in WAF (us-east-1 region).

Resource Reference

Resource Value
AWS Account ID 398673484771
S3 Bucket menotime-playbook-site (us-west-1)
CloudFront Distribution E3INI5YRMUS5XY
CloudFront Domain dpd3q7c2751s3.cloudfront.net
Custom Domain menotime-playbook.timelessbiotech.com
OAC ID E1N4BLOW004B7E
OIDC Provider token.actions.githubusercontent.com
IAM Role GitHubActionsDeployRole
ACM Certificate 27063a57-bdd0-48f0-b39a-ddf98d149a1f (us-east-1)
GitHub Repo TimelessBiotech/menotime-playbook (branch: main)

Security Considerations

  • The S3 bucket has no public access. Content is only accessible through CloudFront.
  • GitHub Actions uses OIDC federation, so no long-lived AWS credentials are stored in the repository.
  • The IAM role trust policy is scoped specifically to the menotime-playbook repo on the main branch.
  • WAF IP restriction ensures only authorized networks can reach the site.
  • CloudFront enforces HTTPS for all traffic.
  • Rotate the WAF IP set whenever your team VPN or office IPs change.