CloudFront + S3 Static Delivery with OIDC Deploys
Private origin, security headers, smart cache invalidation and keyless CI deploys
Basis: Portfolio implementation with a demo site. Not presented as client work.
Overview
A production-shaped static hosting stack: a private S3 bucket reachable only through CloudFront Origin Access Control, security response headers, a tested URL-rewrite function, ACM and Route 53 automation, and GitHub Actions deploying through OIDC with a role scoped to one bucket and one distribution.
Problem
Static sites are often served from public buckets, deployed with long-lived access keys stored in CI, cached so aggressively that releases don't show up — or invalidated with a wildcard on every deploy.
Solution
Keep the bucket private and let only this distribution read it through OAC. Deploy from GitHub Actions with short-lived OIDC credentials. Fingerprinted assets are cached for a year and never invalidated; HTML is revalidated; each deploy invalidates only the paths that changed.
Architecture
Viewer
Route 53
alias A / AAAA
CloudFront
TLS 1.2+, HTTP/2 + 3
Edge logic
CloudFront Function
/path/ → index.html, 301 for no slash
Response headers policy
HSTS, CSP, nosniff
Origin
Origin Access Control
SigV4, SourceArn-scoped
Private S3 bucket
versioned, owner-enforced
Delivery
GitHub Actions
OIDC, no stored keys
Deploy plan
uploads, deletes, invalidations
Failure and recovery path
- A bad release is rolled back by restoring the previous S3 object versions and invalidating the changed paths.
- Missing pages return the custom 404 with a short error-cache time, so a page published moments later appears quickly.
- The deploy role cannot read or delete object versions, so a compromised pipeline cannot erase rollback history.
Technology stack
- Amazon CloudFront
- Origin Access Control
- CloudFront Functions
- Amazon S3
- ACM
- Route 53
- IAM OIDC (GitHub Actions)
- CloudWatch Logs
- Terraform
Implementation
- Terraform modules for the site (bucket, distribution, OAC, function, headers, certificate, DNS, logging) and the GitHub OIDC deploy role, plus a validated example.
- The URL-rewrite function is specified in Python and the JavaScript is executed with Node against the same 19 cases, which must produce identical output.
- Deploy planner: computes uploads and deletes from a manifest, sets Cache-Control per file type, orders uploads so HTML goes last, and picks minimal invalidation paths with a wildcard threshold.
- Header checker verifies a captured response against the required security header set and an HSTS max-age of at least a year.
- 129 unit tests; the CI deploy job is a disabled template that only becomes active once the role exists.
Security
- Bucket policy allows s3:GetObject only to the CloudFront service for this distribution's ARN; all public access is blocked.
- OIDC trust requires the sts audience and a subject pinned to the main branch or the production environment of one repository.
- Deploy permissions are limited to object writes on one bucket and invalidations on one distribution.
- Terraform checks catch a certificate or WAF ACL created in the wrong region (both must be in us-east-1 for CloudFront).
Reliability
- Upload order — hashed assets, then other files, then HTML — means viewers never get HTML referencing assets that are not there yet.
- Versioned bucket provides rollback without rebuilding.
Cost
- The first 1,000 invalidation paths a month are free and a wildcard counts as one path — the planner switches to a wildcard only past a threshold (figures in the cost doc are illustrative).
- Long-lived caching of fingerprinted assets keeps origin requests and data transfer down.
Results
No quantified results are published for this project. Figures from client work are confidential, and this portfolio only shows numbers that can be verified or reproduced.
Code & documentation
Lessons learned
- Cache strategy is part of the build: fingerprinted file names are what make year-long caching safe.
- OIDC removes the most common CI secret — long-lived cloud keys.
- Known gaps, stated in the README: nothing has run in AWS, the Node test proves logic rather than CloudFront runtime acceptance, and multipart or KMS-encrypted objects are re-uploaded each deploy.