Account Strategy

AWS Intermediate/Advanced

Chapter 1 · Multi-Account Foundations

AWS Fundamentals taught you to build in one account. That is the right way to learn, and the wrong way to run anything that matters. The first decision a team makes, before any server or database, is how many AWS accounts it will have and who may do what in each. This chapter covers the tools for that decision: AWS Organizations (the structure), service control policies (the guardrails), IAM Identity Center (how people sign in), Control Tower (a ready-made version of all of it), and tags (how you find out who is spending what). It ends with an account layout for a small team that you can check with a script.

How every example in this chapter was verified
There is no AWS account and no AWS CLI on the machine that wrote this course, so nothing here was run against AWS. The labels used throughout:
  • Run here (level 1): Python 3.14.0 and cfn-lint 1.57.2 on Windows 11, in a scratch folder outside this site. Output is pasted from the real run.
  • Needs an AWS account (level 2): none in this chapter. Nothing here creates, changes or costs anything.
  • From the documentation, not run (level 3): everything about how AWS itself behaves. I read the AWS Organizations, IAM Identity Center, Control Tower and Billing pages on 2026-10-12 through a page-fetch tool that summarises pages, so quotes below are as that tool returned them. AWS changes limits and defaults often; check the current page before you rely on a number.
Two scripts below are models of the documented rules, not AWS. When one says "allowed", that means "allowed by my reading of the documentation", and the chapter says so each time.

Why One Account Is Not Enough

An AWS account is a boundary. Everything inside it shares one bill, one set of service quotas, one root user and one blast radius: a mistake or a stolen key anywhere in the account can reach everything else in it. Putting production, testing, experiments and your audit logs in one account means a developer experimenting with a test database sits one wrong click from your customers' data, and the bill is one number nobody can split by team. Separate accounts are the strongest dividing line AWS gives you. The reasons teams give, which are reasoning rather than measurements:

ConcernOne accountSeveral accounts
Blast radiusA bad deploy, a leaked key or a deleted stack can touch every environmentDamage stops at the account boundary unless you built a path across it
AccessPolicies must keep prod and dev apart inside one set of users and rolesDifferent people hold different access per account; production can be very few people
BillingOne bill; splitting it relies on every resource being taggedEach account is a line on the bill without any tagging at all
LimitsService quotas are shared, so a load test can use up production's headroomQuotas are generally per account, so environments do not compete
Audit and complianceLogs live next to the things being logged, and can be deleted by the same adminsLogs go to a separate account that workload admins cannot reach

The price is administration: ten accounts means ten places to set up the same baseline. That is exactly what the rest of this chapter removes.

AWS Organizations

AWS Organizations lets you create and group accounts and apply policy to the groups. The vocabulary:

TermMeaning
OrganizationThe whole structure, with exactly one root at the top
Management accountThe account that created the organization. It pays the bills and manages the structure
Member accountEvery other account in the organization
Organizational unit (OU)A folder of accounts and other OUs. Policies attached to an OU apply to everything beneath it
PolicyA document attached to the root, an OU or an account. Service control policies are the type this chapter covers

All features, not just billing. An organization can be set up for consolidated billing only, or with all features. The documentation is explicit that SCPs "are available only in an organization that has all features enabled" and "aren't available if your organization has enabled only the consolidated billing features". Use all features.

Limits to design around

These are the default quotas from the Organizations quotas page (read 2026-10-12). Several are adjustable through Service Quotas; the hard ones are marked.

LimitValue on 2026-10-12Adjustable?
Accounts in an organization10 by default (newly created organizations may start lower)Yes, on request from the management account, up to 50,000 depending on qualification
Roots1No
OUs in an organization2,000Not stated
OU nestingFive levels deep under the rootNot stated
SCP size10,240 charactersNot stated
SCPs attached directly to one root, OU or account10, "all policy limits are hard limits"; every entity must keep at least oneNo
Tags on a root, OU or account50Not stated
Member accounts created at once5Not stated

The 10-account default matters more than it looks: a closed account keeps counting until it is permanently closed, and an invitation you send counts too, according to the same page. A layout with eleven accounts needs a quota increase requested before you create the eleventh. The layout checker in Exercise 3 warns about this.

Keep the management account empty

The Organizations best-practice page says to use the management account "only for tasks that require the management account" and to "store all of your AWS resources in other AWS accounts". One reason it gives is the next section's subject: SCPs "do not work to restrict any users or roles in the management account", so anything you run there is outside your guardrails. The same page recommends limiting who can reach it, reviewing that list quarterly or monthly, and delegating services such as CloudFormation StackSets to a member account where the service supports it.

Service Control Policies

A service control policy (SCP) sets the maximum permissions for the users and roles in the accounts it covers. The documentation is clear about the part people get wrong: "SCPs do not grant permissions". An SCP is a ceiling. Nobody gets anything from an SCP; they get it from an identity-based or resource-based policy, and only if the SCP ceiling also allows it.

SCPs doSCPs do not
Restrict IAM users and roles in member accounts, including each member account's root userAffect anything in the management account
Apply to member accounts that are delegated administratorsAffect service-linked roles
Combine with permission boundaries: the boundary, the SCP and the identity policy must all allow the actionDirectly affect resource-based policies, or users and roles from accounts outside the organization
Apply to everything beneath the OU or root they are attached toRestrict a short list of tasks, such as registering for the Enterprise support plan as the root user

Allow lists and deny lists

When SCPs are enabled, AWS attaches an AWS-managed policy called FullAWSAccess to every root, OU and account. It allows everything, so by default nothing changes. You then choose a strategy:

  • Deny list. Keep FullAWSAccess and attach SCPs that Deny specific things (leaving the organization, turning off logging, unapproved regions). Everything not denied stays allowed. This is the easier one to run.
  • Allow list. Detach FullAWSAccess and attach SCPs that Allow only the services you approve. Everything else is denied, including new AWS services until you add them. This is tighter and more work. The documentation warns: "You should not remove the FullAWSAccess policy unless you modify or replace it with a separate policy with allowed actions, otherwise all AWS actions from member accounts will fail".

Two examples straight from the documentation's own pages (the first is the one AWS now attaches by default to organizations created in the console, see below):

{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyLeaveOrganizationAndCloseAccount", "Effect": "Deny", "Action": [ "organizations:LeaveOrganization", "account:CloseAccount" ], "Resource": "*" } ] }
{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyAllOutsideEU", "Effect": "Deny", "NotAction": [ "cloudfront:*", "iam:*", "route53:*", "support:*" ], "Resource": "*", "Condition": { "StringNotEquals": { "aws:RequestedRegion": [ "eu-central-1", "eu-west-1" ] } } } ] }

The second is a deny list in a clever disguise: Deny with NotAction and StringNotEquals reads as "deny everything except these global services, when the region is not one of these two". The excepted services (iam, cloudfront, route53, support) are the ones the documentation's example leaves out of the region lock. Reading double negatives slowly is a core SCP skill.

New in 2026: a default SCP for console-created organizations
The Organizations documentation says that organizations created through the console after July 10, 2026 automatically get SCPs enabled and the first policy above attached at the root, so member accounts cannot leave the organization or close themselves without action from the management account or a delegated administrator. It does not apply to organizations created with the CLI, SDKs or CloudFormation, or to older ones: those need it attached by hand. AWS recommends keeping it at the root.

The syntax rules, and a checker for them

SCP syntax is IAM policy syntax with restrictions. These rules are stated on the SCP syntax page; the checker below applies them (my implementation of the documentation, not AWS's validator):

RuleWhat the docs say
VersionMust be "2012-10-17"
EffectAllow or Deny
Action / NotActionservice:Action strings; * and ? wildcards
ResourceRequired (or NotResource). In an Allow statement only "*" is accepted; a Deny may name ARNs
ConditionAllowed in both Allow and Deny
Principal, NotPrincipalNot supported
SizeEvery character counts against 10,240; the console strips extra whitespace, the CLI and SDKs save the text exactly as given

Run here (level 1). I wrote 17 SCP files: five taken from or modelled on the documentation, nine with one deliberate mistake each, and three for the size and duplicate-key edge cases. The checker's verdicts:

py -X utf8 scp_lint.py scp/*.json
FAIL bad_action_format.json 237 chars as written, 112 compact error: R9 statement 1: Action value 's3' is not service:Action error: R9 statement 1: Action value 'ec2:Describe Instances' is not service:Action FAIL bad_allow_with_arn.json 189 chars as written, 111 compact error: R8 statement 1: Allow with Resource 'arn:aws:s3:::my-bucket/*'; only '*' is allowed FAIL bad_effect_typo.json 167 chars as written, 89 compact error: R4 statement 1: Effect is 'Denied' FAIL bad_empty_statement.json 52 chars as written, 39 compact error: R3 Statement missing, empty or not an object/array FAIL bad_no_resource.json 136 chars as written, 72 compact error: R6 statement 1: no Resource or NotResource FAIL bad_no_version.json 136 chars as written, 64 compact error: R2 Version is None, must be '2012-10-17' FAIL bad_old_version.json 165 chars as written, 87 compact error: R2 Version is '2008-10-17', must be '2012-10-17' FAIL bad_principal.json 195 chars as written, 103 compact error: R7 statement 1: Principal is not supported in SCPs FAIL bad_trailing_comma.json error: R1 not valid JSON: Illegal trailing comma before end of array (line 1 column 94) PASS good_allow_list.json 402 chars as written, 200 compact PASS good_default_controls.json 323 chars as written, 184 compact PASS good_protect_role.json 513 chars as written, 276 compact PASS good_region_lock.json 570 chars as written, 242 compact PASS good_single_statement_object.json 104 chars as written, 104 compact PASS size_fits_only_compact.json 18932 chars as written, 10222 compact warn: R10 18932 chars as written is over 10240, but 10222 without whitespace fits: console strips whitespace, CLI/SDK saves it as given FAIL size_too_big.json 11022 chars as written, 11022 compact error: R10 11022 chars even without whitespace is over the 10240 limit PASS warn_duplicate_key.json 114 chars as written, 87 compact warn: R1 duplicate key(s) ['Effect']: docs do not say how AWS treats them

Two lines are worth reading closely. size_fits_only_compact.json is 18,932 characters as written (indented) but 10,222 without whitespace: it fits the 10,240 limit only if the whitespace is stripped, which the console does and the CLI does not. That is a practical trap: a policy that saves fine from the console can be refused when the same file is sent with aws organizations create-policy. And the duplicate Effect key passes with a warning, because the documentation does not say what AWS does with it (Node, Python and jq all keep the last, see Working with JSON 11). I did not guess.

Linting the template that deploys an SCP

You will usually deploy SCPs with infrastructure as code (AWS Intermediate/Advanced 4 covers CloudFormation). AWS::Organizations::Policy carries the SCP in a Content property. This template is the default-controls policy, and cfn-lint passes it:

AWSTemplateFormatVersion: "2010-09-09" Description: Attach the default-controls SCP (deny LeaveOrganization and CloseAccount) to a target. Parameters: TargetId: Type: String Description: Root, OU or account ID the SCP is attached to AllowedPattern: "^(r-[0-9a-z]{4,32}|ou-[0-9a-z]{4,32}-[0-9a-z]{8,32}|[0-9]{12})$" Resources: DenyLeaveAndClose: Type: AWS::Organizations::Policy Properties: Name: deny-leave-and-close Description: Member accounts cannot leave the organization or close themselves Type: SERVICE_CONTROL_POLICY TargetIds: - !Ref TargetId Content: Version: "2012-10-17" Statement: - Sid: DenyLeaveOrganizationAndCloseAccount Effect: Deny Action: - organizations:LeaveOrganization - account:CloseAccount Resource: "*"

Run here (level 1). I linted that template, then six broken copies. cfn-lint 1.57.2 said:

py -X utf8 lint_templates.py
=== OK (unchanged): exit status 0 (no findings) === policy Type misspelt: exit status 2 E3030 'SERVICE_CONTROL_POLICIES' is not one of ['AISERVICES_OPT_OUT_POLICY', 'BACKUP_POLICY', 'BEDROCK_POLICY', 'CHATBOT_POLICY', 'DECLARATIVE_POLICY_EC2', 'INSPECTOR_POLICY', 'NETWORK_SECURITY_DIRECTOR_POLICY', 'RESOURCE_CONTROL_POLICY', 'S3_POLICY', 'SECURITYHUB_POLICY', 'SERVICE_CONTROL_POLICY', 'TAG_POLICY', 'UPGRADE_ROLLOUT_POLICY'] tmpl_variant.yaml:14:7 === Name removed: exit status 2 E3003 'Name' is a required property tmpl_variant.yaml:11:5 === TargetIds removed: exit status 4 W2001 Parameter TargetId not used. tmpl_variant.yaml:4:3 === Content is a string: exit status 0 (no findings) === Principal inside Content: exit status 0 (no findings) === Effect misspelt in Content: exit status 0 (no findings)

cfn-lint caught the misspelt policy type and the missing Name, and only hinted at the removed TargetIds (it noticed the parameter was no longer used, which is not the same as knowing a target is missing). It missed all three mistakes inside Content: a string instead of a policy, a Principal, and Effect: Denied all gave exit status 0. For this version, the template linter checks the shape of the CloudFormation resource but not the policy language inside it. That is why the SCP checker above is a separate step, and why neither tool replaces testing on one account first (see below). Not run: other cfn-lint versions.

How SCPs combine

The evaluation page gives the rules, and they are worth stating exactly:

  • Allow needs a yes at every level. "There must be an explicit Allow statement at every level from the root through each OU in the direct path to the account (including the target account itself)."
  • Deny needs a no at any level. "Any SCP from the root through each OU in the direct path to the account (including the target account itself) can deny that permission." A deny at the root beats an allow below it.
  • Several SCPs at the same level add together. Scenario 7 on the page has two allow lists at the root, one for IAM and EC2 and one for S3 and CloudWatch, and the accounts get all four services.

I turned the page's scenarios into a small model and ran them. The "docs say" column is my reading of each scenario's prose (the figures are images I could not see, so for scenarios 1 and 4 I assumed the accounts not mentioned as having extra SCPs have only FullAWSAccess). The model is the evaluator code below; it handles Action, NotAction, and multiple SCPs per level, and refuses to guess about Condition.

def level_verdict(scps, action): allow = any(s["Effect"] == "Allow" and stmt_hits(s, action) for p in scps for s in p) deny = any(s["Effect"] == "Deny" and stmt_hits(s, action) for p in scps for s in p) return allow, deny def scp_allows(path_levels, action): """path_levels: list (root ... account) of lists of policies (each policy = list of statements).""" for lv in path_levels: allow, deny = level_verdict(lv, action) if deny or not allow: return False return True def effective(path_levels, identity_allow, action, management=False): if management: return match(identity_allow, action) # SCPs do not apply return scp_allows(path_levels, action) and match(identity_allow, action)

Run here (level 1). The model against the documentation's scenarios (a set of five probe actions: ec2:RunInstances, s3:GetObject, iam:CreateRole, cloudwatch:PutMetricData, lambda:InvokeFunction):

py -X utf8 scp_eval.py
scenario account docs say allowed model says 1 deny at OU and account A cloudwatch,ec2,iam,lambda cloudwatch,ec2,iam,lambda match 1 deny at OU and account B cloudwatch,iam,lambda cloudwatch,iam,lambda match 2 allow-list at the OU A ec2 ec2 match 2 allow-list at the OU B ec2 ec2 match 3 root has only a Deny A (none) (none) match 4 layered deny, two OU levels D (Test) cloudwatch,iam,lambda,s3 cloudwatch,iam,lambda,s3 match 4 layered deny, two OU levels E (Production) cloudwatch,ec2,iam,lambda,s3 cloudwatch,ec2,iam,lambda,s3 match 6 root deny beats lower allow D (Test) (none) (none) match 6 root deny beats lower allow E (Production) cloudwatch,ec2,iam,lambda cloudwatch,ec2,iam,lambda match 7 two allow-list SCPs at the root A cloudwatch,ec2,iam,s3 cloudwatch,ec2,iam,s3 match mismatches: 0 of 10 control (one expectation flipped on purpose): mismatches 1 of 10

All ten account results match what the documentation says. The control line shows the comparison can fail: with one expectation flipped on purpose, exactly one mismatch is reported. The rest of the run shows the two ideas people get wrong:

py -X utf8 scp_eval.py (continued)
-- the SCP grants nothing: same account, three identity policies no identity policy s3:GetObject -> False, ec2:RunInstances -> False identity policy allows s3:* s3:GetObject -> True, ec2:RunInstances -> False AdministratorAccess (*) s3:GetObject -> True, ec2:RunInstances -> True -- same account, but an SCP Deny on ec2, with AdministratorAccess ec2:RunInstances -> False, s3:GetObject -> True -- the management account with the same deny ec2:RunInstances -> True (SCPs do not apply there) -- what the model refuses to guess NotModelled: Condition

An account with no identity policy can do nothing even though the SCP allows everything (the ceiling is not a grant); an administrator with * still cannot run EC2 once an SCP denies it; and the same deny does not touch the management account. This is a model of the documented rules. It says nothing about how real requests are evaluated once conditions, resource policies, boundaries or session policies join in: AWS Intermediate/Advanced 2 takes the full evaluation order.

Test an SCP on one account first
The documentation says AWS "strongly recommends that you don't attach SCPs to the root of your organization without thoroughly testing the impact". Create an OU, move one account in, attach the SCP there, and use CloudTrail or IAM's "service last accessed" data to see what the account really uses before widening it. An SCP that blocks a service a pipeline depends on fails the pipeline, not the person who wrote the policy.

Signing In: IAM Identity Center

In AWS Fundamentals 2 you met IAM users, with passwords and access keys. With many accounts that does not scale: a user per person per account, each with their own long-lived keys. AWS IAM Identity Center is the replacement for people (workforce users). The documentation describes it as the way to "manage centrally your workforce access to AWS accounts":

  • One sign-in point. Users sign in at the AWS access portal and see the accounts and roles assigned to them. You can connect an existing identity provider (it supports SAML 2.0) or create users in Identity Center itself.
  • Permission sets. A permission set is a reusable bundle of permissions for a job function such as administrator or read-only. You provision permission sets across accounts and assign them to users and groups. The documentation says this feature "is available only for organization instances".
  • Organization instance. The documentation calls this "the best practice": it is deployed in the management account, and it is the only kind of instance that manages access to AWS accounts. An account instance is bound to one account and is for isolated application use.
  • Old name. It was AWS Single Sign-On until 2022-07-26. The CLI commands and API namespaces still use sso (for example aws configure sso), so older tutorials still apply.

The usual reason for this design is that people work with short-lived credentials from the portal rather than permanent access keys. That is how it is commonly described and how I understand it; the Identity Center pages I read did not contain that sentence, so I am not quoting it. Level 3 for everything here: setting it up needs an Organizations management account, and I have none. On a machine with the CLI you would run aws configure sso and answer its prompts; I did not run it and did not check its current wording.

AWS Control Tower

Everything above can be done by hand. AWS Control Tower does a recommended version of it for you. The documentation says it "orchestrates the capabilities of several other AWS services, including AWS Organizations, AWS Service Catalog, and AWS IAM Identity Center, to build a landing zone in less than an hour". A landing zone is the multi-account environment it builds: OUs, accounts and baseline settings. The other pieces:

  • Controls (guardrails). High-level rules, in three kinds: preventive, detective and proactive. The page I read does not say which mechanism each kind uses, so I do not claim one.
  • Account Factory. A template for creating new accounts that already match your standards.
  • Dashboard. Shows accounts, enabled controls and non-compliant resources.

The documentation's own advice: "If you are hosting more than a handful of accounts, it's beneficial to have an orchestration layer". For a team of three with four accounts, building the structure by hand from this chapter is reasonable and teaches more; for a company with dozens, use Control Tower. Level 3: not run. I did not check what the service costs beyond the resources it creates, so check the pricing page before enabling it.

Tags and Cost Allocation

Accounts split the bill by team and environment for free. Tags split it inside an account and across accounts (for example "everything for the shop product"). A tag is a key and a value on a resource. Billing's rules, from the Cost allocation tags page:

  • A tag does nothing for billing until you activate it as a cost allocation tag in the Billing and Cost Management console. User-defined and AWS-generated tags are activated separately. User-defined tags appear with a user: prefix, AWS-generated ones with aws:.
  • "All tags can take up to 24 hours to appear in the Billing and Cost Management console."
  • In an organization, only the management account (or a standalone account) can reach the cost allocation tags manager.
  • The cost allocation report "includes both tagged and untagged resources", and its total reconciles with the Bills page. Untagged spend does not vanish; it is a line you can measure.
  • "As a best practice, don't include sensitive information in tags."

Tag policies (a policy type in Organizations, up to 10 attached per entity) can require tag keys and values to follow a standard. I did not read their syntax page, so I do not show one.

Measuring tag coverage

The point of tagging is to find the gaps. Here is the whole technique on invented data: a fixed-seed generator makes a 48-row file shaped like a cost report (account, service, tag columns, cost). The dollar amounts are not AWS prices; only the method is real. The script groups the cost by tag with an explicit "(untagged)" bucket, checks that every grouping adds up to the file total, and lists the biggest untagged rows.

py -X utf8 cost_report.py
first three lines of costs.csv: account,service,user:owner,user:env,cost_usd acme-management,Amazon EC2,platform,mgmt,0.71 acme-management,Amazon S3,platform,mgmt,2.27 acme-management,Amazon RDS,platform,mgmt,0.65
py -X utf8 cost_report.py (continued)
rows 48, total 1,643.56 (synthetic dollars) -- by owner tag shop 1,018.79 62.0% (untagged) 376.22 22.9% platform 146.17 8.9% security 102.38 6.2%
py -X utf8 cost_report.py (continued)
-- the untagged rows, biggest first (where the next tagging effort should go) acme-shop-prod AWS Lambda 171.84 acme-shop-prod Amazon CloudWatch 84.68 acme-shop-staging Amazon EC2 65.66 acme-shared-services Amazon EC2 29.48 acme-shop-dev AWS Data Transfer 14.14 totals reconcile: owner buckets = account buckets = env buckets = file total -- control: drop one row from the data before grouping, and the reconciliation must fail file total 164356 cents, after dropping a row 164285 cents -> DETECTED

About a fifth of the rows had lost their owner tag by design, and they hold 22.9% of the spend. The unrelated groupings (by account, by environment) are unaffected, which is the argument for doing both: accounts give you a total that cannot be mistagged, tags give you the cross-cutting view. The last line is a control: dropping one row changes the total, and the reconciliation catches it.

A Layout for a Small Team

A good first layout is boring. Separate the things that must never be touched by workload admins (logs, security tools), the things shared by everyone (network, pipelines), the workloads by environment, and a place to experiment. Eight accounts for one product:

py -X utf8 layout_check.py
Root (management account: acme-management) ├── Security/ SCPs: deny-leave-and-close, deny-disable-logging │ • acme-log-archive │ • acme-security-tooling ├── Infrastructure/ SCPs: deny-leave-and-close │ • acme-shared-services ├── Workloads/ SCPs: deny-leave-and-close, region-lock-eu │ ├── Prod/ SCPs: deny-delete-backups │ │ • acme-shop-prod │ └── NonProd/ SCPs: - │ • acme-shop-staging │ • acme-shop-dev └── Sandbox/ SCPs: deny-leave-and-close, region-lock-eu, deny-expensive-services • acme-sandbox
OUWhy it existsSCPs in this plan
SecurityLog archive and security tooling. Workload admins get no access, so the evidence cannot be altered by the people it is evidence aboutleave/close denied; a deny on turning off logging
InfrastructureShared services: networking, CI/CD runners, shared imagesleave/close denied
Workloads / ProdProduction, with few people and the strictest guardrailsleave/close and region lock (inherited), plus protection of backups
Workloads / NonProdStaging and development, where people may try thingsinherits from Workloads
SandboxThrow-away experiments, with budget alerts and a block on expensive servicesleave/close, region lock, deny expensive services

Notice what the nesting buys. An SCP attached to Workloads reaches Prod and NonProd without being attached to either, and the "Number of policies you can attach" limit only counts policies attached directly, not inherited ones. The management account sits at the root and holds no workload. The plan names five SCPs; this chapter wrote two of them as files (the leave/close one and the region lock), so the rest are names in a plan, not tested policies.

Run here (level 1). The checker passes this layout and rejects the five broken copies I made (a missing tag, a workload in the management account, a shared e-mail address, an eleventh account, OUs nested six deep):

py -X utf8 layout_check.py (continued)
=== the layout: 8 accounts, 6 OUs, deepest OU path 2 -> PASS === broken 1: new account without a cost-center tag: 9 accounts, 6 OUs, deepest OU path 2 -> FAIL error: L4 x is missing tags ['cost-center'] === broken 2: workload placed in the management account: 8 accounts, 6 OUs, deepest OU path 2 -> FAIL error: L3 management account acme-management hosts a workload === broken 3: two accounts share one e-mail: 8 accounts, 6 OUs, deepest OU path 2 -> FAIL error: L6 duplicate e-mail aws+shop-prod@example.com === broken 4: 11 accounts: 11 accounts, 6 OUs, deepest OU path 2 -> PASS warn: L2 11 accounts is over the default quota of 10: a Service Quotas increase must be requested first === broken 5: OUs nested six deep, and an empty OU chain: 8 accounts, 12 OUs, deepest OU path 6 -> FAIL error: L1 OU nesting is 6 levels, limit 5 warn: L7 OU L0 has no account under it warn: L7 OU L0/L1 has no account under it warn: L7 OU L0/L1/L2 has no account under it warn: L7 OU L0/L1/L2/L3 has no account under it warn: L7 OU L0/L1/L2/L3/L4 has no account under it warn: L7 OU L0/L1/L2/L3/L4/L5 has no account under it

The 11-account copy passes with a warning rather than failing, because the quota is a default that can be raised. Two of the checker's rules are mine rather than AWS's: the required tags are a team convention, and the check for duplicate e-mail addresses is a sanity check (I did not find an e-mail rule on the pages I read, so I do not claim one).

Cost and Clean-Up

Nothing in this chapter creates anything in AWS, so there is nothing to delete and nothing to pay. When you do build this for real, start with a budget alert (AWS Fundamentals 9) on the management account, create the OUs and accounts, and test each SCP on the sandbox account before the root. I did not check what AWS Organizations, Identity Center or Control Tower charge for themselves (some of it may be free, and the resources they create may not be): read the pricing pages before enabling them.

What was and was not checked
Run here: the SCP checker (17 files), cfn-lint on one template and six broken copies, the evaluation model against ten documented results, the layout checker (one layout and five broken ones) and the tag report on invented data. From the documentation, not run: every statement about AWS behaviour, limits and defaults (read 2026-10-12, possibly summarised by the fetch tool), the July 10, 2026 default SCP, Identity Center and Control Tower. Not run at all: anything in a real organization, aws organizations commands, the access portal, Control Tower, tag activation, and any other version of the tools used.

Practice

Exercise 1

Write a checker for SCP files that applies the documented syntax rules (version, effect, action format, resource rules for Allow, no Principal, size both as written and with whitespace removed). Run it on five good policies and ten with one mistake each, and report the size trap. Then lint a CloudFormation template that deploys an SCP, along with six broken copies, and record which mistakes cfn-lint catches and which it misses.

📄 View solution
Exercise 2

Model the documented SCP evaluation rules (an allow at every level, a deny at any level, union of SCPs at one level, management account exempt) and run the documentation's scenarios as tests. Write your expected results first, count mismatches, and add a control that flips one expectation. Show that an SCP grants nothing and that the model refuses to guess about conditions.

📄 View solution
Exercise 3

Describe an account layout for a small team as JSON and write a checker (OU depth, account count against the default quota, no workload in the management account, required tags, distinct names, unknown OUs and SCP names). Show it rejecting five broken layouts. Then group an invented cost file by tag, measure the untagged share, and prove the totals reconcile.

📄 View solution

Chapter 1 Quick Reference

  • One account is one blast radius, one bill and one set of quotas; separate accounts are the strongest boundary and the bill splits itself
  • Organizations: one root, a management account, member accounts, OUs (five levels deep at most); use all features, because SCPs do not exist in consolidated-billing-only mode
  • Default quotas (2026-10-12): 10 accounts (adjustable), 10,240-character SCPs, 10 SCPs attached per root, OU or account; closed accounts count until permanently closed
  • Keep workloads out of the management account: SCPs do not apply to it
  • An SCP is a ceiling, never a grant. Allowed = an Allow at every level from root to account, no Deny anywhere, and an identity or resource policy that grants it
  • SCPs apply to member-account roots too; they do not touch service-linked roles or the management account; Allow statements accept only Resource: "*"; no Principal
  • Deny list = keep FullAWSAccess, deny specifics (easy). Allow list = replace FullAWSAccess with approved services (tight). Never remove FullAWSAccess without a replacement
  • Whitespace counts toward the size limit unless the console strips it, so a policy can save from the console and be refused by the CLI
  • Console-created organizations after July 10, 2026 get a leave/close deny at the root; older or CLI/CloudFormation-created ones need it attached by hand
  • IAM Identity Center (organization instance in the management account) gives people portal sign-in and permission sets per account; CLI namespace is still sso
  • Tags only reach the bill after activation, and take up to 24 hours; measure the untagged share and make buckets reconcile
  • cfn-lint 1.57.2 checks an SCP resource's shape but not the policy inside Content; lint the policy separately and test on one account first
Coming next
Chapter 2 is the IAM deep dive: the full policy evaluation order (where SCPs, permission boundaries, session policies, identity and resource policies each sit), conditions and policy variables, roles and STS, cross-account access, and the tools for proving a policy does what you think. Start with AWS Intermediate/Advanced 2.