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
- Push a change to the main branch of TimelessBiotech/menotime-playbook.
- Go to the Actions tab in GitHub to confirm the workflow runs successfully.
- Open https://menotime-playbook.timelessbiotech.com in a browser.
- Verify the playbook site loads correctly.
- 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.comtodpd3q7c2751s3.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/3273.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.