IAM policies decide what a principal may do inside one account. That works until you have fifty accounts and fifty teams who each hold administrator access to their own. At that point you need rules that an account administrator cannot remove: never disable the audit trail, never leave the organization, never create resources outside approved Regions. AWS Organizations provides those rules as service control policies, attached above the accounts where no one inside them can reach.
This article covers how to structure the organization, how SCPs combine as they flow down the tree, what they cannot touch, how to choose between deny lists and allow lists, a baseline set of policies with real JSON, resource control policies for the other half of a data perimeter, and a rollout process that does not lock your company out of its own cloud. The complete IAM evaluation order is in AWS IAM, condition operators in IAM condition keys and the wider account-vending picture in cloud landing zones; this page builds on them.
The organization as a tree
An organization has one management account, which creates it and pays the bill, and any number of member accounts. Accounts sit in organizational units, OUs can nest up to five levels below the single root, and policies attach to the root, to OUs or to individual accounts. The default quota is 10 accounts, raised on request. SCPs and the other authorization policies are only available when the organization has all features enabled, not just consolidated billing.
The account is the strongest isolation boundary AWS offers: separate quotas, separate IAM, separate blast radius. Organizations makes it cheap to have many of them, and SCPs make it safe, because rules attached to an OU apply to every account placed in it, including accounts created next year.
How SCPs combine
An SCP never grants a permission. It sets a ceiling on what IAM users and roles in member accounts can be granted. For an action to be allowed, three things must hold at once: some SCP at every level from the root down to the account allows it, no SCP anywhere on that path denies it, and the principal's own IAM policies allow it. If you also use permission boundaries, the boundary must allow it too. A Deny at any level wins, and a missing Allow at any level is an implicit deny, even for a role with AdministratorAccess.
That is why every root, OU and account starts with the AWS-managed FullAWSAccess SCP attached. It allows everything, so the ceiling is open until you add denies. Every entity must have at least one SCP attached while SCPs are enabled, and the documentation warns not to remove FullAWSAccess unless you replace it with another allow, or every action in the affected accounts fails.
- SCPs apply to the member account's root user as well as to its IAM users and roles.
- SCPs do not apply to the management account, to service-linked roles, or to principals from other accounts acting on your resources through resource-based policies.
- SCPs apply to member accounts registered as delegated administrators, which is one reason to delegate security tooling out of the management account.
- SCP statements cannot use
PrincipalorNotPrincipal; to exempt a role, use a condition onaws:PrincipalArn.
Deny lists versus allow lists
With a deny-list strategy you leave FullAWSAccess in place and attach SCPs that deny specific actions. New AWS services are available automatically, policies stay short, and each statement documents one rule. With an allow-list strategy you replace FullAWSAccess with SCPs that allow only named services, so anything not listed is implicitly denied at that level. It is tighter, but every new service a team needs becomes a policy change, and an allow list must appear at every level of the path, which makes the tree harder to reason about.
Most organizations use deny lists everywhere and an allow list only for constrained OUs such as a sandbox or a regulated workload. Deny statements can also target specific resource ARNs and use conditions freely, which is what guardrails need.
A baseline policy set
The following SCP, attached at the root, covers rules that almost every organization wants. It is compact on purpose: each SCP is limited to 10,240 characters and each target to 10 directly attached SCPs, so related rules belong together. The exempted role is a break-glass and pipeline role that platform administrators can assume.
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "NoLeavingOrg", "Effect": "Deny",
"Action": "organizations:LeaveOrganization", "Resource": "*" },
{ "Sid": "ProtectAuditTrail", "Effect": "Deny",
"Action": ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail", "cloudtrail:UpdateTrail",
"guardduty:DeleteDetector", "guardduty:DisassociateFromAdministratorAccount",
"config:StopConfigurationRecorder", "config:DeleteConfigurationRecorder"],
"Resource": "*",
"Condition": { "ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/OrgPlatformAdmin" } } },
{ "Sid": "NoRootUser", "Effect": "Deny", "Action": "*", "Resource": "*",
"Condition": { "ArnLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" } } },
{ "Sid": "ApprovedRegionsOnly", "Effect": "Deny",
"NotAction": ["iam:*", "organizations:*", "sts:*", "support:*", "cloudfront:*",
"route53:*", "budgets:*", "health:*"],
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": ["eu-west-1", "eu-central-1"] },
"ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/OrgPlatformAdmin" } } }
]
}The Region statement uses NotAction because global services such as IAM, Organizations and CloudFront are served from a fixed Region regardless of where the caller works, and denying them would break the whole account. The exemption list in your organization must come from your own CloudTrail data, not from this example; note that ACM certificates for CloudFront must be requested in us-east-1, so add a scoped ACM exception or approve us-east-1 for it, as the worked example below shows. Conditions with several keys are ANDed, so the deny applies only when the Region is unapproved and the caller is not the platform role. Before trusting any condition, check what happens when its key is absent from the request; the condition keys article explains where missing keys make policies fail open.
Before denying security services, make sure no legitimate automation needs those actions; an AWS service managing them on your behalf through a service-linked role is not affected by SCPs.
OU design that keeps policies simple
Design OUs around the policies they need, not around the org chart. Teams reorganize; the difference between production and sandbox does not. A layout that works for most companies has a Security OU for log archive and security tooling accounts, an Infrastructure OU for shared networking, Workloads split into Prod and NonProd, a Sandbox OU with tight spending and service limits, a Suspended OU that denies everything for accounts being closed, and a Policy Staging OU where new SCPs are tried first.
Keep the management account empty of workloads. It is the one account SCPs cannot constrain, so anything compromised there is unconstrained. Use delegated administration so GuardDuty, Security Hub, Config aggregation and similar services are run from a security account instead, where SCPs do apply.
Resource control policies: the other half of the perimeter
SCPs constrain your principals wherever they go. They do nothing about someone else's principal reaching into your bucket through a bucket policy that a developer loosened by mistake. Resource control policies cover that side: they are attached in the same tree, but they set the maximum permissions on resources in your member accounts, whoever calls. They support a defined list of services, including S3, KMS, STS, SQS and Secrets Manager.
Custom RCPs can only use Deny, must set Principal to "*", must name a service in each action rather than a bare wildcard, and do not support NotAction. Each is limited to 5,120 characters and five per target, and the undetachable RCPFullAWSAccess counts toward those five. The classic use is an organization boundary on data.
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "S3OnlyFromMyOrg", "Effect": "Deny", "Principal": "*",
"Action": ["s3:*"], "Resource": "*",
"Condition": {
"StringNotEqualsIfExists": { "aws:PrincipalOrgID": "o-exampleorgid" },
"BoolIfExists": { "aws:PrincipalIsAWSService": "false" } } }
]
}This denies S3 access to any principal outside the organization unless the caller is an AWS service principal, such as CloudTrail delivering logs. Real perimeters need further exceptions, for example partners you share data with deliberately, so treat this as a starting shape to test, not a policy to paste. Like SCPs, RCPs do not apply to the management account or to service-linked roles, and they do not apply to AWS managed KMS keys.
A rollout pipeline that cannot lock you out
An SCP mistake at the root can stop every deployment in the company within seconds, and the change takes effect without any deploy on the receiving side. Treat policies as code: keep them in a repository, validate them in CI, apply them with automation, and promote them through the tree.
import json, pathlib, subprocess, boto3
MAX_SCP_CHARS = 10_240
org = boto3.client("organizations")
def check(path: pathlib.Path) -> str:
doc = json.loads(path.read_text())
body = json.dumps(doc, separators=(",", ":")) # minified, as stored via the API
if len(body) > MAX_SCP_CHARS:
raise SystemExit(f"{path}: {len(body)} chars > {MAX_SCP_CHARS}")
out = subprocess.run(["aws", "accessanalyzer", "validate-policy",
"--policy-type", "SERVICE_CONTROL_POLICY",
"--policy-document", body], capture_output=True, check=True)
errors = [f for f in json.loads(out.stdout)["findings"] if f["findingType"] == "ERROR"]
if errors:
raise SystemExit(f"{path}: {errors}")
return body
def stage(path: pathlib.Path, staging_ou: str) -> str:
body = check(path)
pol = org.create_policy(Name=path.stem, Description="staged", Type="SERVICE_CONTROL_POLICY",
Content=body)["Policy"]["PolicySummary"]
org.attach_policy(PolicyId=pol["Id"], TargetId=staging_ou)
return pol["Id"]Then promote in steps: the staging OU with representative test accounts, then NonProd, then one Prod account, then the Prod OU, then the root. At each step run your deployment pipelines and smoke tests and query CloudTrail for access-denied errors that mention a service control policy; the error message for an SCP denial says so explicitly. Use the IAM service last accessed data for each OU to find services that are allowed but never used before turning a deny list into tighter rules.
Worked example: the Region guardrail that broke certificates
A company restricted all workloads to eu-west-1 with an SCP that denied every action outside that Region, with exemptions only for IAM and STS. It was attached to the Workloads OU on a Friday. On Monday a team could not renew the TLS certificate for its CloudFront distribution: certificates used by CloudFront must be in us-east-1, and the ACM request there was denied. A second team found its CloudFront and Route 53 changes failing for the same reason.
The fix was three changes. The Region statement gained the global services that the team's own CloudTrail showed in use, plus ACM actions scoped to us-east-1 by a second condition. The platform role exemption was added so an operator could repair a broken account without detaching the policy. And the rollout moved to the staged pipeline above, so the next guardrail spent a week in the staging OU before anyone else saw it.
Failure modes and quotas
| Failure | What happens | Prevention |
|---|---|---|
| FullAWSAccess removed without a replacement allow | Every action in affected accounts fails | Lint for an Allow at each level before detaching |
| SCP policy type disabled on the root | All SCP attachments are removed and are not restored when re-enabled | Restrict organizations:DisablePolicyType; keep attachments in code |
| Policy too large or too many attached | API rejects it; teams split rules ad hoc | Track 10,240 characters and 10 SCPs per target in CI |
| Workloads in the management account | SCPs do not constrain them | Move them out; keep only org administration there |
| Condition key missing from some requests | Deny silently does not apply | Test with requests that lack the key; use IfExists or Null deliberately |
| External principals reach your data | SCPs do not cover them | RCPs plus resource policies with aws:PrincipalOrgID |
Guardrails complement detection rather than replacing it. Record configuration with AWS Config so drift shows up even where an SCP cannot reach, and protect key policies as described in AWS KMS, because RCPs do not cover AWS managed keys.
Trade-offs
Every SCP you add is a rule that some team will eventually run into, and the error they see is an access denial they cannot fix themselves. Too few guardrails leaves the audit trail and the data perimeter at the mercy of every account administrator; too many turns the platform team into a ticket queue. Prefer a small number of strong, well-tested denies at the root, stricter rules only where an OU's risk justifies them, exemptions through a single audited platform role, and detective controls for everything that does not need to be prevented outright.
What to do next
- List every account and confirm the management account runs no workloads; plan to move any that it does.
- Draw your OU tree around policy needs and create a Policy Staging OU and a Suspended OU.
- Write the baseline SCP above in code, adjusted from your own CloudTrail service usage, and validate it with Access Analyzer in CI.
- Promote it through staging, NonProd, one Prod account, the Prod OU and the root, checking access-denied events at each step.
- Create and protect a break-glass role exempted by aws:PrincipalArn, and test that it works.
- Pilot an RCP that limits S3 and KMS access to your organization on one OU, with your known external partners exempted.