AWS Organizations and SCPs With AI: Guardrails That Actually Deny
Service control policies are easy to write and easy to get wrong. Here's how to use AI to design an OU structure and deny boundaries without locking yourself out of your own accounts.
- #aws
- #ai
- #organizations
- #scp
- #governance
The first SCP I ever shipped to production denied ec2:RunInstances outside two regions, and within an hour a team in a third region was paging me because their pipeline couldn’t launch a build runner. The policy was technically correct and operationally wrong, and that gap is the whole story of governance in AWS Organizations. Service control policies don’t grant permissions — they set the ceiling on what any principal in an account can do, including the account root. Get the ceiling right and nobody notices. Get it wrong and you’ve either left a hole open or locked a team out of work they’re allowed to do. AI is genuinely useful here because the policy language is small but the blast radius is large, and a model can reason about the deny logic and the OU placement faster than I can hold all of it in my head. What it can’t do is know which teams are about to spin up in a new region, so I keep the design loop tight: AI drafts the structure and the policy JSON, I verify against the accounts that actually exist.
The line I hold: AI proposes the OU tree and the SCP boundaries with the explicit deny logic spelled out. I decide what we actually attach and where, because the model doesn’t know our roadmap, our break-glass procedures, or which “temporary” account is secretly load-bearing.
Start from the OU tree, not the policies
SCPs attach to organizational units and accounts, and they inherit down the tree. If you don’t get the OU structure right first, every policy becomes a special case. Look at what you have before designing what you want:
# The whole tree, roots down to accounts
aws organizations list-roots --query 'Roots[0].Id' --output text
aws organizations list-organizational-units-for-parent \
--parent-id r-abcd \
--query 'OrganizationalUnits[].{Name:Name,Id:Id}' --output table
# Where each account actually lives
aws organizations list-accounts \
--query 'Accounts[?Status==`ACTIVE`].{Name:Name,Id:Id,Email:Email}' \
--output table
A structure that holds up over time usually separates accounts by trust boundary and lifecycle rather than by team name. A common shape: a Security OU for log archive and audit accounts, an Infrastructure OU for shared networking and tooling, a Workloads OU split into Prod and NonProd, a Sandbox OU with loose guardrails, and a Suspended OU for accounts on their way out. The point is that you attach a strict deny boundary to Workloads/Prod and a looser one to Sandbox without rewriting policies per account.
Write deny boundaries, not allow lists
The single most important SCP design decision: prefer deny statements with conditions over allow lists. An SCP that only allows a curated set of actions will silently break the next service AWS launches, and you’ll spend your Friday figuring out why a new feature returns AccessDenied with no IAM policy in sight. Deny statements are surgical — they take away specific things and leave everything else governed by IAM.
Here’s a region-restriction boundary that denies actions outside approved regions but carves out the global services that genuinely have no region:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"sts:*",
"organizations:*",
"cloudfront:*",
"route53:*",
"support:*",
"waf:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2"]
}
}
}
]
}
The NotAction block is the part people forget. Global services run “in” us-east-1 under the hood, and denying them by region will break IAM, CloudFront, and Route 53 in ways that look like a different problem entirely.
A second boundary worth attaching to prod: protect the controls that keep you safe. Deny anyone — including account admins — from disabling CloudTrail, GuardDuty, or the org’s config recorder, and from leaving the organization:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectSecurityControls",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"config:DeleteConfigurationRecorder",
"config:StopConfigurationRecorder",
"organizations:LeaveOrganization"
],
"Resource": "*"
}
]
}
Let AI reason about the deny logic
The trap in SCP authoring is the interaction between a deny statement and your break-glass role. A boundary that denies everything outside a region will also deny the emergency access path you assumed would always work. This is exactly the kind of reasoning where a model earns its place — it can walk the condition logic against a specific principal and tell you where you’ve cornered yourself.
Prompt: I’m attaching this SCP to our Workloads/Prod OU. It denies all actions when
aws:PrincipalTag/teamis absent. We have a break-glass roleOrganizationAccountAccessRoleassumed via the management account during incidents. Does this policy lock that role out? Walk the evaluation.
Reply: Yes, it locks it out. SCPs apply to every principal in the account except the service-linked roles AWS excludes — and
OrganizationAccountAccessRoleis not exempt. Your deny triggers wheneveraws:PrincipalTag/teamis missing, and a role assumed cross-account from the management account won’t carry that session tag unless theAssumeRolecall passes it. During an incident, whoever assumes break-glass almost certainly won’t be setting team tags. The result: your emergency access path returnsAccessDeniedexactly when you need it. Two fixes — either add aConditionwithArnNotLikeonaws:PrincipalArnto exempt the break-glass role ARN, or scope the deny to specific high-risk actions instead of*. I’d do both: exempt break-glass explicitly so the exemption is auditable, and narrow the action set so a tagging gap can’t brick a whole account.
That’s the pattern in one exchange — the model caught the cross-account session-tag detail and the break-glass interaction, and I verified both against our actual role ARN before attaching anything. For more boundary-design patterns, see the AWS guides index, and the governance and policy prompts collection has reusable starting points for this kind of review.
Test before you attach
Never attach an SCP straight to a prod OU. Move a single throwaway account into a test OU, attach there, and exercise the workflows you care about. The CLI to wire it up:
# Create the policy
aws organizations create-policy \
--name "deny-outside-approved-regions" \
--type SERVICE_CONTROL_POLICY \
--description "Region guardrail with global-service carve-outs" \
--content file://region-boundary.json
# Attach to a test OU first
aws organizations attach-policy \
--policy-id p-exampleid \
--target-id ou-test-1234
# Verify what's effective on a specific account
aws organizations list-policies-for-target \
--target-id 123456789012 \
--filter SERVICE_CONTROL_POLICY \
--query 'Policies[].Name' --output table
Detaching is just as easy if something breaks, but only if you haven’t denied organizations:DetachPolicy to yourself from the management account — which is why those org-management actions belong governed at the management account, never inside the boundary you’re testing.
Keep the inheritance visible
The thing that bites teams six months later is forgetting that SCPs stack. An account in Workloads/Prod/PaymentsTeam inherits the boundary at the root, at Workloads, at Prod, and at PaymentsTeam — and the effective permission is the intersection of all of them with IAM. When an AccessDenied shows up with no matching IAM deny, the answer is almost always an SCP three levels up. A short habit that pays off: document, per OU, what each attached SCP is for in one sentence, and have AI summarize the combined effect when you stack a new one. I keep that summary in the same repo as the policy JSON so the next engineer reads the intent before the syntax. If you’re building out broader account governance, the AWS Well-Architected review walkthrough pairs well with this — guardrails are the security pillar made concrete.
The boundaries that survive are boring: deny the dangerous things, carve out the global exceptions, exempt break-glass explicitly, test on a throwaway account, and write down why. AI makes the drafting and the deny-logic reasoning fast, but the decision about what ceiling your prod accounts live under is yours — own it, and verify every condition against the principals that actually exist.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.