Organizations and SCPs

Guardrails that even account admins cannot cross.

Intermediate30 min · lesson 3 of 6

You own an apartment building. Inside each apartment, the tenant decides who gets a key to which door. That was the last lesson. IAM (Identity and Access Management) hands out keys inside a single AWS account, deciding which principal may call which API (application programming interface, the individual operations a service exposes). But a few rules have to hold no matter whose key it is. Nobody unplugs the smoke detectors. Nobody welds the fire exit shut. Those rules belong to the building, and no tenant, not even the one paying the most rent, gets to vote them away.

In AWS, the building is an organization and the building rules are service control policies (SCPs). AWS Organizations is the service that ties many AWS accounts into one tree. One account, the management account, creates the organization. Every other account is a member account. You sort members into organizational units (OUs), nestable folders that hang off a single root. An SCP is a small JSON (JavaScript Object Notation, the plain-text format AWS writes policies in) document you attach to the root, to an OU, or to one account. It sets a hard cap on what anyone inside those accounts can ever do, and it is enforced from outside the account, so nobody inside can loosen it.

The one attack IAM can't stop

IAM is excellent at one job: deciding whether a given principal may make a given call. It has a blind spot, and it's a serious one. IAM cannot defend an account against its own administrators. A role with AdministratorAccess can do anything, including the things you would never want done. Say a credential gets phished, or an engineer on their last day decides to burn the place down. They switch off CloudTrail so nothing is recorded, delete the trail to be thorough, then spin up a fleet of pricey instances in ap-southeast-1 to mine cryptocurrency where nobody is watching. Every one of those calls is legal under AdministratorAccess. IAM says yes to all of them.

SCPs close that blind spot, because they sit above the account. An SCP restricts every principal in a member account, including the account's root user (the original, unrestricted login created with the account itself). Ordinary IAM policies and permission boundaries can't touch the root user. SCPs can. And a rule you write at the organization level cannot be edited, detached, or deleted by anyone inside the member account, no matter how much access they hold. That is the entire point: a rule that survives a full account compromise.

A ceiling, never a key

Here is what people get backwards. An SCP never grants a permission. It only takes permissions off the table. It works like the ceiling height in a room: it caps how high you can reach, but it puts nothing in your hand. What you can actually pick up is whatever your IAM policy gives you and the ceiling still allows. In set terms, your effective permissions are the intersection of what the SCPs allow and what IAM grants.

So an SCP that allows s3:* gives a brand-new role precisely nothing, because IAM still has to grant S3 too. And an IAM policy that allows s3:* is dead weight if no SCP in scope allows S3. Both gates have to say yes. This is why a new organization ships with an AWS-managed SCP named FullAWSAccess attached to everything the instant you switch SCPs on. It is an allow-everything ceiling, so nothing changes until you deliberately lower it.

How the tree decides a request

Attachment flows down the tree, and the two effects, allow and deny, follow opposite rules. Think of reaching one specific apartment. You pass the building's front door, then the floor's hallway door, then the apartment's own door, and every one of them has to be unlocked or you don't get in. An SCP allow works the same way. For an action to clear the SCP layer, some policy has to allow it at every level along the path: the root, each OU in between, and the account itself. Miss an allow at any single level and the action is denied, even when every other level allows it. That is why pulling FullAWSAccess off one OU with nothing to replace it instantly blocks everything beneath that OU.

Deny is simpler, and harsher. One locked door anywhere on that route stops you cold, no matter how many others stand open. One explicit Deny in any SCP on the path kills the action, and nothing lower down can override it. A Deny at the root is therefore unremovable from below. That single property is the whole reason SCPs work as guardrails: the person you're defending against lives inside the account, and they cannot reach up the tree to delete your rule.

How a request clears the SCP gate
1Principal makes a call
e.g. an AdministratorAccess role runs cloudtrail:StopLogging
2Allowed at every level?
Some SCP must allow it at the root AND each OU AND the account, or it's denied
3Any explicit Deny on the path?
One Deny in any SCP up the tree blocks it, with no appeal from below
4Does IAM grant it too?
An identity policy must also allow the call
5Call runs
Only when the SCP ceiling and IAM both say yes
SCPs never grant. Effective permission = the SCP ceiling intersected with the IAM grant.

Two exemptions matter in practice, and both can bite. SCPs never apply to the management account. Attach the strictest Deny you can write to the root, and a principal in the management account walks right past it. That's the top reason to keep the management account empty: no workloads, no CI (continuous integration) roles, no daily human logins, nothing worth stealing. The second exemption is service-linked roles (roles that AWS services create to do their own background housekeeping). SCPs skip those, so your region lock won't break AWS's own internal work.

Switch it on and carve an OU

Everything from here runs from the management account, or from a member account you've registered as an Organizations delegated administrator. First, confirm the organization has all features turned on, because SCPs don't exist in the older consolidated-billing-only mode. Then enable the policy type and create an OU to hold your workload accounts.

terminal
# Are all features on? SCPs need "ALL", not "CONSOLIDATED_BILLING"
aws organizations describe-organization --query 'Organization.FeatureSet' --output text
# Find the root's ID (every org has exactly one root)
aws organizations list-roots --query 'Roots[0].Id' --output text
# Turn on the SCP policy type for the whole org
aws organizations enable-policy-type \
--root-id r-8f2k --policy-type SERVICE_CONTROL_POLICY
# Make an OU to hold workload accounts
aws organizations create-organizational-unit \
--parent-id r-8f2k --name workloads
output
ALL
r-8f2k
{
"Root": {
"Id": "r-8f2k",
"Arn": "arn:aws:organizations::111122223333:root/o-a1b2c3d4e5/r-8f2k",
"Name": "Root",
"PolicyTypes": [
{ "Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED" }
]
}
}
{
"OrganizationalUnit": {
"Id": "ou-8f2k-x7a4mq1c",
"Arn": "arn:aws:organizations::111122223333:ou/o-a1b2c3d4e5/ou-8f2k-x7a4mq1c",
"Name": "workloads"
}
}

Write the deny list

Now the policy itself. Keep FullAWSAccess in place as the ceiling and stack targeted Deny statements on top of it. This is the deny-list style, and it's the safe default: anything you forget to mention stays allowed instead of breaking at 3 a.m. Two statements. The first makes the audit pipeline tamper-proof. The second pins all activity to two approved regions.

deny-security-tampering.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectAuditPipeline",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail",
"cloudtrail:PutEventSelectors",
"guardduty:DeleteDetector",
"guardduty:UpdateDetector",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"config:DeleteDeliveryChannel"
],
"Resource": "*"
},
{
"Sid": "LockRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"sts:*",
"cloudfront:*",
"route53:*",
"support:*",
"budgets:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["eu-west-1", "us-east-1"]
}
}
}
]
}

The first statement protects the three services that record what happened: CloudTrail (logs every API call), GuardDuty (flags suspicious activity), and AWS Config (tracks how resources change over time). Look at cloudtrail:PutEventSelectors in that list. It's the quiet one. An attacker who can't call StopLogging can sometimes call PutEventSelectors instead, stripping a trail down until it logs nothing while still showing as 'on'. Deny both, or you've locked the front door and left a window open.

The second statement's NotAction list is the part that flattens an OU if you get it wrong. Some services either run globally or route through regions you never picked, and a blanket region lock can catch them by accident. So you exempt the usual global control planes: IAM, Organizations, STS (Security Token Service, which issues the temporary credentials every assumed role runs on), CloudFront, Route 53, Support, and Budgets. NotAction means 'this Deny applies to every action except these,' so those services stay reachable while everything else is confined to eu-west-1 and us-east-1. STS is the one that bites hardest. It hands out credentials from regional endpoints, so an AssumeRole in ap-southeast-1 really does count as an ap-southeast-1 call. Leave STS out of the exemption and role assumptions in any blocked region start failing, which breaks far more than you intended.

terminal
# Create the policy from the file, print only the new policy ID
aws organizations create-policy \
--name deny-security-tampering \
--type SERVICE_CONTROL_POLICY \
--description "Protect audit pipeline; lock regions" \
--content file://deny-security-tampering.json \
--query 'Policy.PolicySummary.Id' --output text
# Attach it to the workloads OU (this prints nothing on success)
aws organizations attach-policy \
--policy-id p-9dxq4mel --target-id ou-8f2k-x7a4mq1c
# Confirm what is now attached to the OU
aws organizations list-policies-for-target \
--target-id ou-8f2k-x7a4mq1c \
--filter SERVICE_CONTROL_POLICY --query 'Policies[].Name'
output
p-9dxq4mel
[
"FullAWSAccess",
"deny-security-tampering"
]

Prove it actually blocks

A guardrail you haven't tested is a hope, not a control. Assume a role in a member account under the workloads OU, one that carries AdministratorAccess, and try the exact moves the SCP is meant to stop. If it's working, full admin rights won't help.

terminal
# Running in a member account in the OU, as an AdministratorAccess role:
# Statement 1 in action: try to blind the auditors
aws cloudtrail stop-logging --name org-trail
# Statement 2 in action: try to work in an unapproved region
aws dynamodb list-tables --region ap-southeast-1
output
An error occurred (AccessDeniedException) when calling the StopLogging operation: User: arn:aws:sts::444455556666:assumed-role/AdministratorAccess/alice is not authorized to perform: cloudtrail:StopLogging on resource: arn:aws:cloudtrail:eu-west-1:444455556666:trail/org-trail with an explicit deny in a service control policy
An error occurred (AccessDeniedException) when calling the ListTables operation: User: arn:aws:sts::444455556666:assumed-role/AdministratorAccess/alice is not authorized to perform: dynamodb:ListTables on resource: arn:aws:dynamodb:ap-southeast-1:444455556666:table/* with an explicit deny in a service control policy
Never detach FullAWSAccess without a replacement
Detaching FullAWSAccess from the root or an OU with nothing to replace it flips every account underneath into allow-list mode on the spot. Remember the rule: an action needs an allow at every level. Remove the only allow at one level and everything below it is denied instantly, running production included, because nothing there allows anything anymore. Teams have knocked out live systems with that single API call. If you want allow-list SCPs, write the replacement policy first, attach it, rehearse in a throwaway OU, and only then detach the default.

Limits and where it bites

The quotas show up sooner than you'd expect. You can attach at most ten SCPs to any one target (a root, an OU, or an account), and each policy body caps at 10,240 characters. Here's the trap: the CLI (command-line interface) saves your JSON exactly as you typed it, whitespace and all, so every space and line break counts against that 10,240. Only the web console strips the whitespace for you. Larger organizations hit both ceilings, which pushes you toward compressing rules with NotAction and designing your OU layout with intent, rather than writing one SCP per rule until you run out of room.

The debugging story is thin. A blocked call tells you 'with an explicit deny in a service control policy' and stops there. You learn that an SCP did it rather than your own IAM setup, but not which policy, which statement, or which level of the tree pulled the trigger. So keep statements small, give each a clear Sid (statement ID, the label you saw as ProtectAuditPipeline above), and reconcile denials against CloudTrail after the fact. Roll every SCP change through a sandbox OU holding one disposable account before it goes near production. An SCP mistake doesn't fail quietly. It denies in bulk, everywhere the policy reaches, at once.

The wall SCPs don't build

Know exactly where this tool stops. SCPs bound what your principals can do. They say nothing about what outside principals can do to your resources. An S3 bucket with a wide-open bucket policy, readable by the whole internet, sits completely outside an SCP's reach, because the stranger reading it isn't one of your principals at all. So when someone claims an SCP 'blocks public access,' it doesn't, and trusting that it does will hurt you.

Closing that gap is the job of resource control policies (RCPs), a newer Organizations policy type. An RCP is the mirror image of an SCP: attached the same way onto the org tree, but it caps what anyone (including strangers in other accounts) may do to your resources, whatever a resource policy tries to allow. SCPs guard the door from the inside. RCPs guard it from the outside. Real guardrails use both.

Prevention is only half of a control. A guardrail tells you what can't happen. It never tells you what did. You've locked CloudTrail so a compromised account can't silence it from the inside, and the next move is making sure someone is actually reading what it records. The next lesson wires this into an organization-wide trail plus GuardDuty in every member account, so that whatever your SCPs still allow is always, at minimum, being watched.

Quick check
01You attach an SCP to an OU whose only statement is Allow s3:* on Resource *. A role in an account in that OU has no IAM policy attached at all. What can the role do with S3?
Correct — SCPs cap the maximum, they never hand out access, so with no IAM allow the role gets nothing.
Incorrect — an SCP Allow only defines the ceiling; the role still needs an IAM policy that grants s3:*.
Incorrect — there is no read-only default in SCPs, and an SCP grants nothing on its own.
Incorrect — SCPs don't grant access, and there is no per-OU home region that would.
02You attach a Deny-all-CloudTrail-tampering SCP to the organization root. Which principals does it actually restrict?
Incorrect — SCPs never restrict principals in the management account, which is exactly why you keep it empty.
Correct — SCPs bind every principal in member accounts down to their root users, while the management account stays exempt.
Incorrect — SCPs apply to IAM users and to member-account root users too, not only roles.
Incorrect — an SCP on the root inherits down to every OU and account beneath it.
03A member account's CI pipeline suddenly fails on every AWS call in ap-southeast-1 with 'AccessDeniedException ... with an explicit deny in a service control policy.' The same pipeline still works in eu-west-1, and no IAM policy changed. What is the cause, and where do you fix it?
Incorrect — the error names a service control policy and eu-west-1 still works, so the IAM permissions are intact.
Incorrect — detaching FullAWSAccess would deny all regions, not only ap-southeast-1, and re-attaching it isn't region-specific.
Correct — the deny is a region-lock SCP above the account, and SCPs can only be changed from the management account or a delegated admin.
Incorrect — an SCP restricts the member-account root user too, so logging in as root changes nothing.

Related