The problem it exists for
Every new AWS account needs the same four things before anything useful can happen in it: somewhere to keep infrastructure state, a way for CI to sign in without stored keys, guardrails, and alerts for when something goes wrong. Working those out again by hand for every account is, in the words of the module’s own spec, waste. So they’re two modules, applied in order, and the second is built without leaning on anybody’s framework: 52 AWS resources across six parts, and no third-party modules among them.
How it works
First, the bootstrap
The bootstrap module does three things and deliberately stops there. It makes an encrypted state bucket that refuses to be destroyed (with locking inside S3 itself, so no DynamoDB table), a role CI can assume through OIDC from GitHub or GitLab with no stored credentials anywhere, and a generated configuration for aws-nuke, so a throwaway account can be wiped clean on purpose.
Then the baseline
The security baseline is applied through that CI role, and comes in six parts, each of which can be switched off:
- account hardening
- an audit trail in CloudTrail, kept for two years
- a configuration history in AWS Config
- threat detection, with GuardDuty, Security Hub running the AWS best-practice and CIS standards, and Access Analyzer
- alerts for high or critical findings, and for any use of the root login
- an operator role that needs MFA and is locked to one region
Decisions and what they cost
- Keep the bootstrap narrow. State, CI sign-in and the nuke configuration only, instead of one module that sets up the whole account. What it cost: two modules and two version streams to keep in step.
- No frameworks. Cloud Posse, Gruntwork and Control Tower were all considered and turned down, and even the community modules the first draft planned to use for three of the baseline’s parts ended up written by hand. What it cost: every line is mine to maintain, and getting the audit trail’s bucket protected the way I wanted took about fifty more of them.
- Half rewritten. GitLab’s OIDC sign-in was written by hand, but GitHub’s still wraps the community module, because rewriting both would have pushed existing users through a state migration. What it cost: two code paths for one job until 1.0 tidies it up.
- No lock table. State locking uses S3’s own locking instead of DynamoDB. What it cost: it needs OpenTofu, or Terraform 1.10 or later.
Proof in use
- The bootstrap is at v0.3 and the baseline at v0.2, both from July 2026, and both docs sites are live. Each release tag publishes the module to GitLab’s module registry through cicd’s tofu-module-publish component, and cicd’s guide to signing its plan and apply jobs in to AWS can reuse the bootstrap’s CI role.
- Why I hand-rolled every module is the argument behind building them this way.
Use it when, and when not to
Use it if you’re standing up a single AWS account and want the boring, important parts done properly before you build anything in it.
Know what it isn’t. It’s one account at a time: AWS Organizations, Identity Center, WAF, Inspector, Macie and Detective are all out of scope. GuardDuty runs in the main region only. The CI role gets administrator access unless you narrow it, which you should. Security Hub and GuardDuty cost money, and a brand-new account can refuse them until it’s subscribed, which is why each has its own switch. And it’s below 1.0, so pin a tag.
Where it’s going
The region lock the baseline gives its operator role is meant to reach the bootstrap’s CI role too, and separate plan and apply roles with a permission boundary are on the roadmap. Neither has been built yet.
What it covers
The story in posts
- The runner fleet that scales to zero
- Wire up an AWS billing alarm before the surprise
- The security service I had to switch off
- Two bugs that taught me the rules
- Reviewed, then applied
- One graph, not micro-stacks
- CI you include, not copy
- One image for the whole toolchain
- A 403 you can't fix in IAM
- Routing security findings without the noise
Last reviewed .