AWS Hosted Portfolio with Automated Deploys
This website: a private S3 bucket behind CloudFront, a custom domain with ACM TLS, and keyless CI/CD through GitHub Actions and OIDC.
- AWS S3
- CloudFront
- ACM
- IAM
- OIDC
- GitHub Actions
- Cloudflare DNS
- CloudTrail
Goal
Host my portfolio on AWS for close to zero cost while applying the same security controls I would expect in production.
Architecture
- Private origin: the S3 bucket blocks all public access, with SSE-S3 encryption and versioning. CloudFront reads it through Origin Access Control, so the bucket is never exposed directly.
- HTTPS everywhere: CloudFront redirects HTTP to HTTPS, using a free ACM certificate for ekeneeche.com validated through DNS.
- Custom domain: registered on Cloudflare, pointed at CloudFront with DNS only CNAME records and CNAME flattening at the root.
Keyless deployment
- Every push to the site folder triggers a GitHub Actions workflow that syncs files to S3 and invalidates the CloudFront cache.
- GitHub authenticates to AWS through OpenID Connect, so no long lived access keys are stored anywhere. Credentials last about an hour.
- The deploy role trusts only my repository's main branch and can only write to one bucket and invalidate one distribution.
Incident: first deploy failed
- The first run failed with
Not authorized to perform sts:AssumeRoleWithWebIdentity. - I pulled the rejected event from CloudTrail and compared the token's subject claim with the role's trust policy. GitHub now includes immutable account and repository IDs in the subject, which the console generated policy did not match.
- I rewrote the trust policy with an exact
StringEqualsmatch on the new format. This fixed the deploy and tightened the policy, since numeric IDs cannot be reused the way names can.
Takeaways
Least privilege and short lived credentials are practical even for a small project, and audit logs turn a vague access error into a precise root cause.