Note 08
Guardrails an account inherits on day one
Organization-wide SCP and RCP guardrails consolidated from per-account copies into one baseline per OU, with offline tests and a dry run against real CloudTrail history.
- AWS
- Security
- Terraform
How guardrails rot
Guardrails grow account by account: the same rule copied with a different list of ARNs, accounts at the five-policy quota, new accounts with none at all. Nobody can say what an account actually has without reading every policy on its path.
One rule, one place
Each rule lives once, at the highest level of the organization where it is true:
- Root: the audit trail cannot be stopped, the root user can do nothing, nobody leaves the organization, and the tags that grant privilege through ABAC can only be set by security admins.
- Workloads OU: every role has a permissions boundary, regions are locked, IMDSv2 and encryption are required, interactive host access goes through the access proxy.
- Production OU: only CI publishes images, only autoscalers start instances, only code changes cluster access and network edges.
- A resource control policy keeps S3, Secrets Manager and KMS closed to principals outside the organization and to plain HTTP, whatever a resource policy says.
SCPs have no audit mode
A broken SCP is found by an outage. So every change passes three gates. Offline tests check the quota, that every statement is a Deny, and that the break-glass role keeps a way in. A replay of ninety days of CloudTrail lists every principal the candidate would have denied. And new policies are attached next to the old ones for 48 hours before anything is removed, OU by OU, production last; policies combine as a union of denies, so this never opens a gap.
run "guardrails_only_deny" {
command = plan
assert {
condition = alltrue(flatten([
for p in aws_organizations_policy.scp : [for s in jsondecode(p.content).Statement : s.Effect == "Deny"]
]))
error_message = "An SCP contains an Allow statement; guardrails only take permissions away."
}
}The replay is the gate that pays for itself: it catches the change that would have stopped node autoscaling in production because one role name did not match a new pattern.
What it changes
Dead and duplicated policies went first, cutting the custom set by a third with no change in effective access. The target layout takes it from nineteen policies to six and from thirteen account-level attachments to one deliberate exception, rolled out OU by OU. The dry run has already paid for itself: it caught a change that would have stopped node autoscaling in production before it shipped.
Start a conversation
Bring your infrastructure
into focus.
Tell me what you are building, operating, or trying to improve.