A curated collection of articles exploring this topic in depth.
Security Boundaries in Small Systems
Why security is about explicit intent rather than system size, and how to implement robust boundaries without enterprise complexity.
Security can be thought of as a binary state: a system is either secure or it is not. For a personal project, this can lead to a "good enough" approach where security is often deprioritised. However, a more productive model is to view security as a series of explicit trust boundaries.
In this architecture, security is not an additional layer; it is an inherent property of the design. By using modern identity primitives and strictly defined resource policies, we can achieve a level of protection that is both robust and low-maintenance.
Identity Without Secrets
The examples below reflect my own implementation, but the principle is what matters: identity should be explicit, short-lived, and tightly scoped.
A common security failure in small (and large!) systems is the mismanagement of static credentials. Storing long-lived AWS Access Keys in a CI/CD environment like GitHub Actions creates a permanent risk of leakage.
To eliminate this risk, this blog uses OpenID Connect (OIDC) for deployment. Instead of static keys, GitHub Actions requests a short-lived token from AWS. This token is only granted because we have established a cryptographic trust relationship between GitHub and our AWS account.
The trust is scoped to a specific repository and a specific branch:
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" : "repo:edwardsmatt/blog:ref:refs/heads/main"
}
}This means that even if the CI/CD pipeline were compromised, the attacker would only have access to the specific resources required for deployment, and only for the duration of the session.
Least Privilege in Practice
Defining a trust relationship is only half of the solution. The other half is ensuring that the assumed role has the minimum permissions necessary to perform its job.
The deployment role for this blog is restricted to three specific actions:
- Listing and syncing files to the specific S3 bucket.
- Deleting old files from that same bucket.
- Creating a cache invalidation for the specific CloudFront distribution.
It cannot create new buckets, change DNS records, or access any other part of the AWS account. This adherence to the principle of least privilege ensures that a failure in one component does not lead to a total system compromise.
Private Origins and Origin Access Control
As discussed in previous posts, the S3 bucket containing the blog content is entirely private. It has no public access points and does not use the S3 "Static Website Hosting" feature.
To allow CloudFront to serve this private content, we use Origin Access Control (OAC). This mechanism ensures that S3 only accepts requests that are signed by our specific CloudFront distribution.
The bucket policy explicitly defines this boundary:
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.site.arn]
}By enforcing this, we prevent bypass attacks where an attacker might attempt to access the origin directly to avoid security headers or caching rules defined at the edge.
Security as Intent
These boundaries are not complex to implement, but they require intentionality. When we use Terraform to define these relationships, the security model becomes a visible and auditable part of the codebase.
We are not relying on "hidden" security features of a managed platform. We are making explicit decisions about who is allowed to do what, and under what conditions. This clarity is what makes a system production-grade, regardless of its traffic or size.
In the next post, we will look at why testing content pipelines is not overkill, but a necessary guardrail for maintaining a long-lived digital garden.