AWS Lesson 62 of 123

Enforcing Org-Wide Guardrails with AWS Organizations, SCPs, and Delegated Administration

Identity policies tell you what a principal can do. Guardrails tell you what nobody in the org can do, no matter what their IAM policy says. That distinction is the whole reason AWS Organizations exists as a security control plane and not just a billing convenience: it lets you draw a ceiling above every account so that a compromised credential, a careless engineer, or a misconfigured pipeline simply cannot perform the action — the request is denied before it ever reaches the resource. This guide builds that ceiling end to end with Service Control Policies (SCPs), Resource Control Policies (RCPs), and declarative policies — the three families of preventive control that ride above every member account — and shows how to delegate your security tooling (GuardDuty, Security Hub, IAM Access Analyzer) out of the management account without engineering a self-lockout.

The reason this is an expert topic and not a tutorial is that every one of these controls is a deny that cannot grant anything back, attached at a scope that the management account is exempt from, evaluated by an intersection rule most people get backwards. Misplace a single NotAction and you sever role assumption org-wide. Forget one BoolIfExists and a service-linked role’s call gets caught in your deny and breaks a nightly job nobody is watching. Protect a role too hard and it becomes permanently unmanageable. The mechanics are unforgiving, the blast radius is the entire organization, and the only safe path to production runs through staging OUs and the policy simulator. So we treat the failure modes as first-class: every guardrail comes with how it bricks you, how to confirm it from inside a member account (never from the exempt management account), and how to recover with break-glass.

By the end you will be able to design a layered policy tree (root for non-negotiables, OUs for tier-specific rules), write the four foundational deny SCPs every org should ship, condition denies on org membership and tags, build an org-wide data perimeter with RCPs, pin EC2 configuration state with declarative policies, delegate regional and global security services correctly, and roll the whole thing out OU-by-OU without ever locking yourself out. Because this is a reference you will return to mid-rollout, the evaluation rules, the condition keys, the service scopes, the size limits, and the failure playbook are all laid out as scannable tables — read the prose once, then keep the tables open while you write the policies.

What problem this solves

In a single AWS account, IAM is enough: you grant least privilege and audit who has what. The moment you have many accounts — and any serious organization has dozens to hundreds — IAM stops being sufficient, because IAM is account-local and grant-shaped. Nothing in IAM stops a workload-account admin from disabling CloudTrail in their own account, sharing a snapshot to an external account, spinning up resources in an unapproved region, or using the account root credentials for daily work. Each account team can perfectly follow least privilege and the org as a whole can still be wide open, because no single account can see or constrain the others.

What breaks without org-wide guardrails is governance at scale. A region-restriction control that should hold for every account has to be copied into every account’s IAM and kept in sync forever — and the first account that forgets it is your compliance gap. The audit pipeline that proves what happened can be turned off by the very accounts it audits. The data perimeter that should say “only my org’s identities touch my data” has to be re-implemented in thousands of individual bucket and key policies, any one of which can be edited to punch a hole. These are not problems IAM can solve, because they are negative universal statements (“nobody, anywhere, ever”) and IAM only makes positive local grants.

Who hits this: every platform/security team responsible for a multi-account AWS estate — which today means almost every mid-size-and-up AWS customer, since the AWS-recommended pattern is one account per workload-environment. It bites hardest the day a security review asks “prove no member account can disable its own CloudTrail” or “prove data can’t be shared outside the org,” and the honest answer with IAM alone is “we can’t.” SCPs, RCPs, and declarative policies are the layer that turns those into one-line, org-enforced, audit-provable guarantees. To frame the whole field before the deep dive, here is every control family this article covers, what it constrains, and the single most important fact about it:

Control family What it constrains Grants? Mgmt acct affected? The one fact people get wrong
Service Control Policy (SCP) The identity side — max permissions of principals in an account No, deny/filter only No (exempt) An “allow” SCP grants nothing; the principal still needs an IAM Allow
Resource Control Policy (RCP) The resource side — caps every resource policy in scope No, deny only No (exempt) Covers only a specific service list (S3, STS, KMS, SQS, Secrets Manager, ECR, OpenSearch Serverless)
Declarative policy Service configuration state (EC2 first) n/a — it sets state Yes — it configures the service everywhere Uses @@assign syntax, not IAM JSON; survives new APIs and new accounts
Identity / resource policy (IAM) The actual grant Yes Yes The only thing that grants; SCP/RCP only subtract from it
Delegated administration Which member account admins a security service n/a Moves admin out of mgmt acct Some services use the Organizations API, some their own regional API

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand single-account IAM: the difference between identity policies and resource policies, how an explicit Deny always wins over any Allow, what a principal ARN looks like, and how condition keys like aws:RequestedRegion work. You should be comfortable in the AWS CLI and reading/writing IAM JSON. Familiarity with the multi-account landing-zone shape — a management (payer) account, a Security OU with Log Archive and Audit accounts, and Workload OUs — is assumed; if that is new, read it first.

This sits at the top of the Governance & multi-account track. It assumes the IAM fundamentals from AWS IAM Fundamentals: Users, Roles, Policies & Evaluation and the least-privilege discipline from IAM Least Privilege with Permission Boundaries. It is the policy layer of the landing zone built in AWS Control Tower: Multi-Account Landing Zone and complements the deeper data-perimeter treatment in Resource Control Policies, Declarative Policies & the Data Perimeter. It pairs with CloudWatch & CloudTrail Observability, because watching CloudTrail for AccessDenied is how you confirm every guardrail actually bites.

A quick map of who owns what during a guardrail rollout, so you route questions correctly:

Layer What lives here Who usually owns it Failure classes it can cause
Management account Organizations, root, landing-zone setup Platform / cloud-foundations team None directly (exempt) — but a wrong policy here looks fine while bricking members
Root attachment Org-wide non-negotiable SCPs/RCPs Platform + security Region-lock lockout, root-user over-deny
OU attachment Tier-specific guardrails (Prod/NonProd/Sandbox) Security + per-tier owners Wrong-OU placement → wrong guardrails inherited
Security OU Log Archive + Audit accounts, delegated admins Security team Audit tampering, delegation gaps (regional)
Member account principals IAM grants under the ceiling App / dev teams AccessDenied they can’t explain (the guardrail bit)
Break-glass role The exempt escape hatch Security (offline creds) Lockout recovery; misuse if not audited

Core concepts

Five mental models make every later decision obvious.

An SCP never grants — it filters. The single most important fact about an SCP, and the one people get wrong: it does not add permissions to anything. It is a filter on the maximum permissions available to principals in an account. The effective permission set for an action is the intersection of every applicable policy:

Effective allow = identity policy (or resource policy)
                INTERSECT  every SCP from root down to the account
                INTERSECT  every RCP   from root down to the account

If an action is not Allowed by an identity policy, an SCP that “allows” it changes nothing — the request is still denied. Flip it around and the SCP becomes powerful: if any SCP in the chain denies the action, no identity policy anywhere can win it back. A request succeeds only when it is allowed by the principal’s own policy and is not blocked by any SCP and is not blocked by any RCP on the target resource.

The management account is exempt — by design, and as a trap. Policies attached at the root do not restrict the management (payer) account. That is a feature (you cannot brick your own org from the top, so there is always one account from which to recover) and a trap (never run workloads there — they are completely unguardrailed). Treat the management account as a billing-and-org control plane only.

Deny model over allow-list. There are two SCP strategies. The default FullAWSAccess policy allows everything, and you attach deny policies on top — this is the maintainable model, because every new AWS service launches usable and you subtract only what you must forbid. The alternative removes FullAWSAccess and explicitly allows services; it is far more brittle because every new service launch is blocked by default and every team files a ticket. Use the deny model. Reserve allow-list SCPs for a tightly-scoped sandbox or a regulated workload OU.

RCPs are SCPs for the resource side. An SCP filters what your principals can do; it says nothing about an external principal hitting your S3 bucket via that bucket’s own policy. RCPs close that gap: deny-only policies that attach to root/OU/account exactly like SCPs but evaluate on the resource side, capping the effective permissions of every resource policy in scope. That is how you build an org-wide data perimeter without editing thousands of bucket and key policies — but only for the services RCPs cover.

Declarative policies set state, they don’t filter calls. Rather than allowing or denying API calls, a declarative policy pins the configuration state of a service across the org, and AWS guarantees the baseline holds even as the service ships new APIs and new accounts join. It uses a different syntax (@@assign) and a different mental model: not “who may call this” but “this is how the service is configured, everywhere, forever.”

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:

Concept One-line definition Where it attaches Why it matters
Organization The container of all your accounts, rooted at one management account n/a — the top object The unit aws:PrincipalOrgID identifies
Root (org root) The top node of the OU tree Policies here apply org-wide (except mgmt acct) Where non-negotiables live
Organizational Unit (OU) A grouping of accounts you attach policy to Policies inherit downward to child OUs/accounts The right granularity for tier rules
FullAWSAccess The default allow-everything SCP Root + every OU/account on creation Remove it only for allow-list models
SCP Identity-side deny/filter policy Root / OU / account Caps principal permissions; ≤ 5 KB
RCP Resource-side deny policy Root / OU / account Caps resource-policy grants; service-scoped
Declarative policy Service config baseline Root / OU / account Sets state (EC2 first); @@assign syntax
aws:PrincipalOrgID Condition key = the caller’s org id In SCP/RCP conditions The core “is this my org?” perimeter check
aws:ViaAWSService True when a service uses your creds In conditions Exempt forward-access-session calls
aws:PrincipalIsAWSService True when a service principal calls directly In conditions Exempt CloudTrail/log-delivery etc.
Delegated administrator Member account that admins a security service Set via API Gets security tooling out of mgmt acct
Break-glass role The exempt escape hatch with offline creds Exempted in every deny Your only safe lockout recovery

How SCPs evaluate: deny-by-intersection, never grant

Everything downstream depends on internalizing the evaluation order, so make it concrete. AWS evaluates a request against several policy types and the result is the intersection: the request must survive every layer. The decisive properties are that an explicit Deny anywhere wins, and that SCPs/RCPs can only ever remove from what IAM granted — never add.

Policy type Acts on Can grant? Affects management account? Default if unattached
SCP Principals (the identity side) No, deny only No FullAWSAccess (allow all)
RCP Resources (the resource side) No, deny only No FullAWSAccess (no extra cap)
Declarative policy Service configuration (e.g. EC2) n/a, sets state Yes, it configures the service Service’s own default
Identity policy Grants to a principal Yes Yes Implicit deny (no grant)
Resource policy Grants on a resource Yes Yes Implicit deny (no grant)
Permissions boundary Caps a principal’s own max No, caps only Yes No boundary (no cap)

The order matters because a single explicit deny short-circuits the whole chain. Walk the decision table for “will this request succeed?”:

Condition in the chain Effect on the request Why
No IAM Allow for the action Denied Nothing grants it; SCP “allow” is irrelevant
IAM allows, but an SCP Deny matches Denied Explicit SCP deny wins over any allow
IAM allows, no SCP deny, but an SCP doesn’t allow it (allow-list model) Denied In an allow-list SCP, absence of allow = deny
Resource policy allows external principal, but an RCP Deny matches Denied RCP caps the resource side
IAM allows, no SCP/RCP deny, action permitted by all SCPs Allowed Survived every intersection
Caller is the management account, root SCP would deny Allowed Mgmt account is exempt from SCP/RCP
Permissions boundary doesn’t include the action Denied Boundary caps the principal’s own max

Two consequences flow from this and shape everything below:

An SCP that “allows” a service does nothing on its own. People write an Allow SCP expecting to grant access and are baffled when it does not work. SCPs only subtract. The principal still needs an IAM Allow; the SCP’s job is to be the ceiling, not the floor.

The management account is exempt from SCPs and RCPs. Policies attached at the root do not restrict the management account — a feature (you cannot brick your own org from the top) and a trap (never run workloads there). This single fact is why you must never test guardrails from the management account: it will look perfectly healthy while every member account is broken.

Step 1 — Designing a layered policy strategy

Guardrails attach at three scopes — root, OU, account — and the discipline is putting each rule at the broadest scope where it is universally true. A control that must hold for every account belongs at the root; a control specific to a workload tier belongs on the OU; account-level attachments exist but are an anti-pattern at scale because they do not survive account moves and become impossible to audit.

Root
 ├── SCP: org-wide non-negotiables (region lock, root lockdown, protect security infra)
 │
 ├── OU: Security        -> tightest; protect log archive + audit roles
 ├── OU: Infrastructure  -> network/shared-services guardrails
 ├── OU: Workloads
 │    ├── OU: Prod       -> deny destructive/escape actions
 │    └── OU: NonProd    -> looser, allow experimentation
 └── OU: Sandbox         -> region + spend guardrails, otherwise open

The trade-offs between the three attachment scopes are not subtle, and choosing wrong creates audit debt that compounds for years:

Attach scope Use for Survives account move? Auditability Anti-pattern risk
Root Controls true for every account (region lock, root lockdown, protect audit/security infra) n/a (root is fixed) High — one place, org-wide Over-broad denies that also hit accounts you forgot need an exception
OU Tier-specific controls (Prod-only destructive denies, Sandbox spend caps) Yes — placement does the work High — policy follows the OU Deep OU nesting making inheritance hard to reason about
Account Almost never; a one-off exception No — breaks silently on move Low — invisible unless you query the account The classic “works until someone reorganizes accounts” failure

How a control’s universality should map to its scope:

If the control must hold for… Attach at Example
Every account, no exceptions Root Deny disabling CloudTrail; region lock
Every account in a workload tier The tier OU Prod: deny iam:CreateUser; deny public S3
Every account except a named platform path Root, with an ArnNotLike carve-out Protect security roles except OrgPlatformAdmin
Only throwaway/experiment accounts Sandbox OU Region + spend guardrails, otherwise open
One specific account, temporarily Account (last resort, document it) A migration exception with an expiry ticket

Before any of this works, enable the policy types on the root (they are off by default):

# Discover the root ID, then enable each policy type
ROOT_ID=$(aws organizations list-roots --query 'Roots[0].Id' --output text)

aws organizations enable-policy-type --root-id "$ROOT_ID" --policy-type SERVICE_CONTROL_POLICY
aws organizations enable-policy-type --root-id "$ROOT_ID" --policy-type RESOURCE_CONTROL_POLICY
aws organizations enable-policy-type --root-id "$ROOT_ID" --policy-type DECLARATIVE_POLICY_EC2

The policy types you can enable, and what each unlocks:

Policy type (API value) What it enables Off-by-default? Enable prerequisite
SERVICE_CONTROL_POLICY SCP attachments at root/OU/account Yes All-features org (not consolidated-billing-only)
RESOURCE_CONTROL_POLICY RCP attachments Yes All-features org
DECLARATIVE_POLICY_EC2 EC2 declarative policy Yes All-features org
TAG_POLICY Tag standardization policies Yes All-features org
BACKUP_POLICY Org-wide AWS Backup plans Yes All-features org
AISERVICES_OPT_OUT_POLICY Opt out of AI-service data use Yes All-features org

Step 2 — Foundational deny SCPs

These are the four guardrails almost every org should ship first. Each is a standalone Deny statement; bundle related ones into a single policy to stay under the 5 KB per-SCP size limit (a real constraint — whitespace counts, so minify in CI). Here is the set, why each exists, and its single most dangerous failure mode:

# Guardrail What it denies Attach at The failure mode if you get it wrong
1 Region restriction Any action outside approved regions Root Omitting iam/sts from NotAction severs role assumption org-wide
2 Root-user lockdown Everything done as the account root Root None major — but scope to member accounts (mgmt root may be needed)
3 Protect audit pipeline Disabling/deleting CloudTrail + Config Root Too narrow an action list leaves a way to blind detection
4 Protect security roles IAM writes against protected roles Root No ArnNotLike exception → role becomes permanently unmanageable

Guardrail 1 — Region restriction

Confine the org to approved regions, but exempt global and global-endpoint services or you will break IAM, Organizations, CloudFront, Route 53, and support. Use aws:RequestedRegion with a NotAction carve-out:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*", "organizations:*", "sts:*",
        "cloudfront:*", "route53:*", "waf:*", "wafv2:*",
        "support:*", "trustedadvisor:*",
        "globalaccelerator:*", "budgets:*", "ce:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["us-east-1", "eu-west-1"]
        }
      }
    }
  ]
}

The NotAction list is load-bearing. Global services have their endpoints in us-east-1; if you omit iam or sts and us-east-1 is not in your allowed set, you can sever the org’s ability to assume roles. Keep us-east-1 allowed even in a single-region EU org, or keep the carve-out exhaustive. Test this one harder than any other policy.

The global/global-endpoint services that must be carved out of a region lock, and why:

Service (action prefix) Why it must be exempt Symptom if you forget it
iam:* IAM is global; endpoint in us-east-1 Cannot create/manage IAM if us-east-1 not allowed
sts:* STS global endpoint authenticates in us-east-1 Role assumption fails org-wide — the worst lockout
organizations:* Organizations is a global service Cannot manage the org itself
route53:* Route 53 control plane is global DNS changes blocked
cloudfront:* CloudFront is a global edge service Distribution changes blocked
waf:*, wafv2:* WAF (CloudFront scope) is global Web-ACL changes blocked
support:*, trustedadvisor:* Support/Trusted Advisor are global Cannot open or manage support cases
globalaccelerator:* Global Accelerator control plane is global Accelerator changes blocked
budgets:*, ce:* Billing/Cost Explorer are global (us-east-1) Budget and cost-management actions blocked

The condition operators you choose for a region lock subtly change its behavior:

Operator Behavior Use when
StringNotEquals on aws:RequestedRegion Deny if region is not in the allow-list The standard region lock
StringNotEqualsIfExists Same, but absent key → no deny If some global calls lack the key and you don’t NotAction them
ArnNotLike on aws:PrincipalArn (add-on) Exempt a break-glass/admin principal Leaving an emergency path through the lock

Guardrail 2 — Root-user lockdown

Member-account root credentials should never be used for day-to-day actions. Deny everything performed by the account root:

{
  "Sid": "DenyRootUserActions",
  "Effect": "Deny",
  "Action": "*",
  "Resource": "*",
  "Condition": {
    "StringLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" }
  }
}

A few root actions genuinely require the root user and cannot be delegated to IAM; know which so you understand what this lockdown implies (these are performed rarely, deliberately, with break-glass-style access, not in daily ops):

Action that needs root Frequency Implication of the lockdown
Change account email / root password Rare Do it via a controlled break-glass process, not daily
Close the AWS account Rare Intentional — workload accounts shouldn’t self-close
Restore an IAM policy that locked out all access Emergency Break-glass scenario; document the path
Configure S3 MFA-delete on a bucket Rare Plan it as a one-off privileged operation
View/modify certain billing settings (legacy) Rare Increasingly delegable; centralize in mgmt acct

Guardrail 3 — Protect CloudTrail and the audit trail

Stop any principal from blinding your detective controls — disabling, deleting, or stopping organization trails and Config:

{
  "Sid": "ProtectAuditPipeline",
  "Effect": "Deny",
  "Action": [
    "cloudtrail:StopLogging",
    "cloudtrail:DeleteTrail",
    "cloudtrail:UpdateTrail",
    "config:DeleteConfigurationRecorder",
    "config:StopConfigurationRecorder",
    "config:DeleteDeliveryChannel"
  ],
  "Resource": "*"
}

The exact actions to deny to keep detection alive, and what each one would otherwise let an attacker do:

Action What it would do if allowed Detection it blinds
cloudtrail:StopLogging Pause API logging on a trail All CloudTrail events stop being recorded
cloudtrail:DeleteTrail Delete the trail entirely Logging gone; org trail removed
cloudtrail:UpdateTrail Re-point or narrow the trail Quietly exclude events / change the bucket
config:DeleteConfigurationRecorder Stop recording resource config Config drift detection dies
config:StopConfigurationRecorder Pause config recording Same, reversibly — easy to miss
config:DeleteDeliveryChannel Stop config snapshots reaching S3/SNS Evidence stops landing in the log archive

Guardrail 4 — Protect security roles and break-glass

A deployment or admin role provisioned by the platform team must not be deletable or modifiable by workload principals. Deny IAM write actions against the protected roles except when the caller is a designated org-admin path, using ArnNotLike on aws:PrincipalArn:

{
  "Sid": "ProtectSecurityRoles",
  "Effect": "Deny",
  "Action": [
    "iam:AttachRolePolicy", "iam:DetachRolePolicy",
    "iam:DeleteRole", "iam:DeleteRolePolicy", "iam:PutRolePolicy",
    "iam:UpdateRole", "iam:UpdateAssumeRolePolicy",
    "iam:PutRolePermissionsBoundary", "iam:DeleteRolePermissionsBoundary"
  ],
  "Resource": [
    "arn:aws:iam::*:role/SecurityAudit",
    "arn:aws:iam::*:role/aws-controltower-*",
    "arn:aws:iam::*:role/OrgBreakGlass"
  ],
  "Condition": {
    "ArnNotLike": {
      "aws:PrincipalArn": "arn:aws:iam::*:role/OrgPlatformAdmin"
    }
  }
}

That exception clause is the difference between a guardrail and a foot-gun. Without it you can lock the role so hard that nobody, including the role’s intended manager, can ever touch it again — and SCPs do not apply to the management account, so your only escape would be moving the account out of the org temporarily. Always leave one authenticated path in. The IAM write actions worth denying on a protected role, and why each is dangerous:

Action Attack it prevents Without the deny
iam:UpdateAssumeRolePolicy Re-pointing the trust policy to an attacker principal Anyone could make the security role assumable by themselves
iam:AttachRolePolicy / iam:PutRolePolicy Privilege escalation by attaching admin to the role Role silently gains *:*
iam:DetachRolePolicy / iam:DeleteRolePolicy Stripping the role’s guardrail policies Role loses its intended limits
iam:DeleteRole Destroying the security/break-glass role Audit/break-glass capability gone
iam:PutRolePermissionsBoundary / iam:DeleteRolePermissionsBoundary Removing/replacing the boundary that caps the role Boundary defeated
iam:UpdateRole Changing role description/max-session subtly Lower-impact, but still tampering

Step 3 — Conditional SCPs with org, tags, and service exceptions

Blanket denies are blunt. The expert move is conditioning a deny so it fires only outside a trusted boundary. The condition keys that do the heavy lifting here, and exactly when each is true:

Condition key True when… Typical use in an SCP
aws:PrincipalOrgID The calling principal belongs to your org Deny actions unless caller is in-org (anti-exfiltration)
aws:PrincipalOrgPaths Caller is under a specific OU path Restrict to/within a particular OU subtree
aws:RequestTag/<key> A tag with that key is supplied on the request Enforce tag-on-create
aws:TagKeys The set of tag keys present in the request Require/forbid specific keys
aws:ResourceTag/<key> The target resource already carries the tag Restrict actions to tagged resources
aws:ViaAWSService A service is calling using your principal’s creds Exempt forward-access-session (FAS) calls
aws:PrincipalIsAWSService A service principal is calling directly Exempt CloudTrail/log-delivery direct calls
aws:SourceOrgID (resource side) The source request originates from your org RCP perimeters

Lock data movement to your org with aws:PrincipalOrgID

This SCP denies sharing actions unless the calling principal belongs to your organization, which neutralizes a whole class of confused-deputy and exfiltration paths:

{
  "Sid": "DenyShareOutsideOrg",
  "Effect": "Deny",
  "Action": [
    "ec2:ModifyImageAttribute",
    "ec2:ModifySnapshotAttribute",
    "rds:ModifyDBSnapshotAttribute"
  ],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": { "aws:PrincipalOrgID": "o-abcd1234ef" }
  }
}

The data-sharing actions worth fencing to your org, by service:

Action What it shares Exfiltration risk
ec2:ModifyImageAttribute Makes an AMI public or shares to an account Whole golden image (and baked secrets) leaks
ec2:ModifySnapshotAttribute Shares an EBS snapshot Full disk contents copied out
rds:ModifyDBSnapshotAttribute Shares an RDS snapshot Entire database copied to a foreign account
s3:PutBucketPolicy (pair with RCP) Opens a bucket to external principals Object data exposed (close on resource side too)
kms:PutKeyPolicy (pair with RCP) Grants external decrypt Ciphertext becomes readable externally

Enforce tag-on-create

Require a cost or owner tag at creation time using aws:RequestTag and aws:TagKeys. The pattern is “deny the create action when the required tag key is absent”:

{
  "Sid": "RequireProjectTagOnInstances",
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "Null": { "aws:RequestTag/Project": "true" }
  }
}

The tag-enforcement condition operators and what each expresses:

Operator + key Fires the deny when… Use to
Null aws:RequestTag/Project = true The Project tag key is absent from the request Require a tag on create
StringNotEquals aws:RequestTag/Env The supplied Env value isn’t in an allowed set Constrain a tag’s allowed values
ForAllValues:StringEquals aws:TagKeys A supplied key is outside the approved key set Forbid arbitrary tag keys
Null aws:ResourceTag/Owner = false The resource already has an Owner tag Gate actions to already-tagged resources
StringNotEquals aws:ResourceTag/CostCenter The target resource’s cost center isn’t yours Restrict ops to your-cost-center resources

Exempt AWS services from a deny

The subtle one. When you write a strict deny, AWS services acting on a principal’s behalf can get caught in it. Two distinct condition keys cover two distinct cases, and conflating them is the most common SCP bug I see in reviews:

Key True when Use to exempt
aws:PrincipalIsAWSService A service principal acts directly on your resource (e.g. cloudtrail.amazonaws.com writing to S3) Service principals making direct calls
aws:ViaAWSService A service makes the call using your principal’s credentials (forward access sessions) A user action that fans out through a service

So a deny that should not apply when, say, Athena or a service-linked role re-drives a request on the user’s behalf gets a BoolIfExists guard:

{
  "Sid": "DenyKmsDeleteExceptViaService",
  "Effect": "Deny",
  "Action": ["kms:ScheduleKeyDeletion", "kms:DisableKey"],
  "Resource": "*",
  "Condition": {
    "BoolIfExists": { "aws:ViaAWSService": "false" }
  }
}

Use BoolIfExists (not Bool) for these keys: if the key is absent from the request context, a plain Bool comparison evaluates unpredictably, whereas ...IfExists treats absence as “the principal called directly.” The distinction in one table:

Comparison If the key is present If the key is absent Verdict
Bool aws:ViaAWSService = false Evaluates normally No match (condition can’t be satisfied) → deny may not apply as intended Unpredictable — avoid
BoolIfExists aws:ViaAWSService = false Evaluates normally Treated as “called directly” → deny applies Correct — use this
Bool aws:PrincipalIsAWSService = false Evaluates normally No match → service-direct calls may slip the deny Avoid
BoolIfExists aws:PrincipalIsAWSService = false Evaluates normally Treated as “not a service” → deny applies to humans Correct

When to reach for which service-exemption key:

Scenario Exempt with Why
CloudTrail writing logs to your S3 bucket aws:PrincipalIsAWSService A service principal calls directly
Log-delivery / config snapshot delivery aws:PrincipalIsAWSService Direct service-principal call
Athena query that re-drives S3 reads on the user’s behalf aws:ViaAWSService Forward access session using user creds
A service-linked role completing a workflow (e.g. RDS, Auto Scaling) aws:ViaAWSService (sometimes both) The SLR fans out with the user’s context
A human running the API directly from the CLI Neither — the deny should apply This is exactly who you’re constraining

Step 4 — Resource Control Policies for the data perimeter

SCPs filter what your principals can do. They say nothing about an external principal hitting your S3 bucket via a bucket policy. RCPs close that gap: they are deny-only policies that attach to root/OU/account like SCPs but evaluate on the resource side, capping the effective permissions of every resource policy in scope. This is how you build an org-wide data perimeter — “only identities from my org, or expected AWS services, may touch my data” — without editing thousands of individual bucket and key policies.

RCPs cover a specific, growing service list. At launch: Amazon S3, AWS STS, AWS KMS, Amazon SQS, and AWS Secrets Manager, with Amazon ECR and OpenSearch Serverless added since. RCPs do not apply to services outside that list, so do not assume one RCP blankets every resource type — confirm the service is in scope before relying on it.

The services RCPs cover and what each protects:

Service What an RCP caps Why it’s in scope first
Amazon S3 Bucket policies / object ACL grants The #1 data-exfiltration surface
AWS STS AssumeRole/trust grants (the identity gateway) Stops external principals assuming in
AWS KMS Key policies / grants (the decrypt gateway) Without decrypt, exfiltrated ciphertext is useless
Amazon SQS Queue policies Cross-account queue access
AWS Secrets Manager Secret resource policies Cross-account secret reads
Amazon ECR Repository policies Pulling private images externally
OpenSearch Serverless Collection data-access policies Search-data exposure

A canonical perimeter RCP: deny access to S3, SQS, KMS, and Secrets Manager resources unless the caller is in your org or is an AWS service. Note the explicit allowance for service principals so CloudTrail, log delivery, and similar do not break:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceOrgIdentityPerimeter",
      "Effect": "Deny",
      "Principal": "*",
      "Action": ["s3:*", "sqs:*", "kms:*", "secretsmanager:*"],
      "Resource": "*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:PrincipalOrgID": "o-abcd1234ef"
        },
        "BoolIfExists": {
          "aws:PrincipalIsAWSService": "false"
        }
      }
    }
  ]
}

Two things make RCPs safe to roll out. First, like SCPs they never grant — adding an RCP can only ever reduce access, so it cannot accidentally open a resource. Second, the management account is again exempt. The risk is the inverse of SCPs: an over-broad RCP can lock legitimate cross-account or service access you forgot about. Stage it (Step 6) and watch CloudTrail for AccessDenied before you widen scope. How RCPs and SCPs differ, side by side:

Aspect SCP RCP
Evaluates on The identity (principal) side The resource side
Caps Principal’s max permissions Resource policy’s effective grants
Stops Your principals doing X External principals reaching your resources
Grants? No (deny/filter) No (deny only)
Service coverage All services A specific list (S3, STS, KMS, SQS, SM, ECR, OSS)
Mgmt account Exempt Exempt
Principal element n/a (acts on the caller) Present ("Principal": "*" etc.)
Default unattached FullAWSAccess RCPFullAWSAccess (no extra cap)

The condition keys that make a perimeter RCP both tight and safe:

Condition key Keeps in Keeps out
aws:PrincipalOrgID (StringNotEqualsIfExists) Every identity in your org Any external account’s principals
aws:PrincipalIsAWSService (BoolIfExists false) AWS service principals (CloudTrail, log delivery) Human/external non-service callers
aws:SourceOrgID Requests originating from your org Cross-org confused-deputy paths
kms:ViaService (KMS carve-out) Service-mediated KMS grants (e.g. RDS) Direct external KMS use
aws:PrincipalAccount (allow-list specific accounts) Named partner accounts you do trust Everyone else

Step 5 — Declarative policies and delegated administration

Declarative policies are a different animal from SCPs and RCPs. Rather than filtering API calls, they pin the configuration state of a service across the org, and AWS guarantees the baseline holds even as the service ships new APIs and as new accounts join. The first supported domain is EC2, where you can enforce VPC Block Public Access, IMDSv2, and block-public-access for EBS snapshots and AMIs — org-wide, in one object:

{
  "ec2_attributes": {
    "vpc_block_public_access": {
      "internet_gateway_block": {
        "mode": { "@@assign": "block-bidirectional" },
        "exclusions_allowed": { "@@assign": "disabled" }
      }
    },
    "instance_metadata_defaults": {
      "http_tokens": { "@@assign": "required" }
    },
    "snapshot_block_public_access": {
      "state": { "@@assign": "block-all-sharing" }
    }
  }
}
# Create and attach the declarative policy to an OU
aws organizations create-policy \
  --name ec2-baseline \
  --type DECLARATIVE_POLICY_EC2 \
  --content file://ec2-declarative.json

aws organizations attach-policy \
  --policy-id p-examplepolicyid \
  --target-id ou-root-workloads

The @@assign operator is declarative-policy syntax, not IAM JSON — that is how the policy sets a value rather than allowing or denying an action. The EC2 attributes a declarative policy can pin, and what each one enforces:

Attribute What it sets Secure value What it prevents
vpc_block_public_access.internet_gateway_block.mode Whether VPCs can route to/from an IGW block-bidirectional Accidental public exposure of any subnet
vpc_block_public_access...exclusions_allowed Whether accounts may opt out per-VPC disabled Teams carving holes in the IGW block
instance_metadata_defaults.http_tokens IMDS version default for new instances required (IMDSv2) SSRF-driven credential theft via IMDSv1
instance_metadata_defaults.http_put_response_hop_limit IMDS hop limit 12 Metadata reachable from containers/proxies
snapshot_block_public_access.state EBS snapshot public-sharing block-all-sharing Public snapshots leaking disk data
image_block_public_access.state AMI public-sharing block-new-sharing / block Public AMIs leaking golden images

How the three control families differ in mechanism — the thing that most confuses people:

Family Mechanism Syntax What “compliance” means Survives new service APIs?
SCP Filters API calls (deny) IAM JSON (Effect/Action) The call is blocked at request time Yes (deny by action) but new actions need policy updates
RCP Filters resource access (deny) IAM JSON with Principal External grants are capped Yes for in-scope services
Declarative Sets configuration state @@assign document The service is configured that way Yes — AWS holds the baseline across new APIs

Delegated administration

Delegated administration gets your security services out of the management account, which you should not be logging into daily. You enable trusted access once, then nominate a member account (typically a dedicated Security or Audit account) as the delegated administrator. The exact API differs by service, and this trips people up:

# IAM Access Analyzer and most services: the Organizations API
aws organizations enable-aws-service-access \
  --service-principal access-analyzer.amazonaws.com
aws organizations register-delegated-administrator \
  --account-id 222233334444 \
  --service-principal access-analyzer.amazonaws.com

# GuardDuty: its OWN API, and it is regional
aws guardduty enable-organization-admin-account \
  --admin-account-id 222233334444 --region us-east-1

# Security Hub: also its own API, also regional
aws securityhub enable-organization-admin-account \
  --admin-account-id 222233334444 --region us-east-1

GuardDuty and Security Hub are regional services delegated through their own enable-organization-admin-account calls — you must repeat the call in every region you operate. IAM Access Analyzer and most other services go through the generic Organizations register-delegated-administrator and only need it once. Mixing these up leaves half your regions unmanaged and is a silent gap auditors love to find.

The delegation mechanism by service — get this table wrong and you have regional blind spots:

Service Delegation API Regional or global? Repeat per region?
IAM Access Analyzer Organizations register-delegated-administrator Global registration No — once
AWS Config Organizations register-delegated-administrator Global registration No — once
CloudFormation StackSets Organizations register-delegated-administrator Global No — once
GuardDuty guardduty enable-organization-admin-account Regional Yes — every region
Security Hub securityhub enable-organization-admin-account Regional Yes — every region
Macie macie2 enable-organization-admin-account Regional Yes — every region
Inspector inspector2 enable-delegated-admin-account Regional Yes — every region
Detective detective enable-organization-admin-account Regional Yes — every region
Firewall Manager fms associate-admin-account Global (one admin) No — once
IAM Identity Center sso-admin / console delegation Global (home region) No — once

Why delegate at all, and to which account each service belongs:

Goal of delegation Typical delegated-admin account What it avoids
Stop logging into the management account daily Security / Audit account Credential exposure of the payer account
Centralize findings org-wide Audit account (GuardDuty, Security Hub, Inspector) Per-account, siloed security views
Centralize access analysis Security account (Access Analyzer) Missing cross-account unused-access findings
Centralize network policy Security/Network account (Firewall Manager) Inconsistent WAF/firewall posture
Centralize identity Identity account (IAM Identity Center) Scattered SSO administration

Step 6 — Safe rollout without self-lockout

The failure mode that keeps platform teams up at night is attaching a deny at the root that locks out the very automation that manages the org. Sequence the rollout to make that impossible. The four-step sequence and what each step buys you:

Step Action What it catches What it cannot catch
1 Stage in a Sandbox OU of throwaway accounts Real terraform apply/workflow breakage under the policy Tier-specific quirks not present in sandbox
2 Simulate with validate-policy + simulate-custom-policy Syntax/grammar errors; obvious action denials Runtime FAS/service-linked-role nuances
3 Promote OU by OU, least-critical first Tier-specific breakage in increasing-stakes order Nothing — this is the safety ramp
4 Never test from the management account The false “looks fine” the exempt account gives n/a — this is a discipline, not a test

1. Stage in a Sandbox OU first

Create an OU containing only throwaway accounts, attach the new SCP/RCP there, and exercise the real workflows your teams run. Nothing learned in a Terraform plan substitutes for watching an actual terraform apply succeed or fail under the policy.

2. Simulate before you attach

Validate the policy document, then dry-run it against real principals and actions with the IAM policy simulator, which understands SCP context:

# Lint the document for syntax/grammar before it ever attaches
aws accessanalyzer validate-policy \
  --policy-document file://scp-region-lock.json \
  --policy-type SERVICE_CONTROL_POLICY

# Simulate: would this principal's action survive the guardrails?
aws iam simulate-custom-policy \
  --policy-input-list file://scp-region-lock.json \
  --action-names "ec2:RunInstances" \
  --context-entries \
    "ContextKeyName=aws:RequestedRegion,ContextKeyValues=ap-south-1,ContextKeyType=string"

The pre-attach validation tools and what each one proves:

Tool / command Validates Catches Limitation
accessanalyzer validate-policy Grammar + best-practice findings Syntax errors, bad ARNs, deprecated patterns Not whether the intent is right
iam simulate-custom-policy A specific action under supplied context Whether the region-lock/tag condition denies Doesn’t model full org inheritance
iam simulate-principal-policy An action for an existing principal Real principal’s effective access Mgmt-account exemption can mislead
CloudTrail AccessDenied review (post-stage) What actually broke in sandbox The runtime FAS/SLR cases simulators miss After the fact — that’s why you stage
organizations list-policies-for-target What policies actually apply to an account Inheritance/attachment mistakes Shows attachment, not request outcome

3. Roll out OU by OU, least-critical first

Promote the same policy outward: Sandbox, then NonProd, then Infrastructure, then Prod, then Security last. The Security OU holds your log archive and audit roles, so it has the least margin for a mistake — it goes when you are most confident, not first.

Order OU Why this position What breaks here is…
1 Sandbox Throwaway accounts; safe to break Cheap to fix; expected
2 NonProd Real workflows, no customer impact A dress rehearsal for prod
3 Infrastructure Shared services (network, DNS) Higher blast radius — proceed carefully
4 Prod Customer-facing; high stakes Expensive — only after the above are clean
5 Security Log archive + audit; least margin Catastrophic if wrong — go last, most confident

4. Never test guardrails from the management account

Because the management account is exempt, a policy can look perfectly fine from there while it has bricked every member account. Always validate from a principal inside a target OU.

The structural insurance against lockout: keep one break-glass role that your SCPs explicitly exempt (the ArnNotLike clause in Step 2), store its credentials offline, and confirm it can still operate after every guardrail change. If a deploy goes wrong, you assume break-glass and fix forward — you never start deleting policies blind.

Architecture at a glance

Read the diagram left to right as the governance build-and-enforce path, because that is the order the controls actually take effect. On the far left, the management plane (the payer/management account) runs AWS Organizations with all features enabled and IAM Identity Center for human access — this account authors every policy and is itself exempt from all of them, which is exactly why nothing runs here. Moving right, those policies attach to the OU policy tree: SCPs as the identity-side ceiling (preventive deny — region lock, root lockdown), RCPs as the resource-side perimeter (deny external principals reaching your S3/STS/KMS/SQS/Secrets Manager), and declarative policies that pin EC2 state (IMDSv2, VPC Block Public Access). Inheritance flows downward, so a rule attached at the root reaches every child account, and a tier-specific rule on the Prod OU reaches only Prod.

The next zone is the Security OU, the separation-of-duties heart of the design: a Log Archive account holding the immutable org CloudTrail bucket (Object Lock WORM) and an Audit account that is the delegated administrator for GuardDuty, Security Hub, and IAM Access Analyzer — protected by the audit-pipeline and security-role SCPs so member accounts can neither blind nor tamper with detection. Finally, the member accounts zone is where workloads actually land, each one sitting under an OU and therefore under the inherited ceiling; AccessDenied events from these accounts flow back to the Log Archive, which is how you confirm a guardrail bit. The numbered badges mark the five places this goes wrong in practice — a region lock without global-service carve-outs, a confused-deputy share, an over-broad RCP, a missing regional delegation, and a self-lockout from an exception-free role deny — each narrated in the legend with how to confirm it from inside a member account and how to recover.

AWS Organizations governance architecture showing the management plane running Organizations and IAM Identity Center, an OU policy tree with SCP guardrails, RCP data-perimeter and declarative EC2 baseline inheriting downward, a Security OU with immutable Log Archive CloudTrail and a delegated-admin Audit account, and member accounts where workloads land with AccessDenied flowing back to the log archive; five numbered badges mark region-lock lockout, cross-org share, over-broad RCP, regional delegation gap and self-lockout failure points

Real-world scenario

A fintech platform team — call them Meridian Pay, running ~140 AWS accounts across two regions (us-east-1, eu-west-1) under a Control Tower landing zone — rolled out the data-perimeter RCP from Step 4 across the org and within an hour their nightly RDS snapshot copy to a dedicated DR account started failing with AccessDenied on the destination KMS key. The on-call SRE’s first instinct was that the RCP was too broad — it was not. The copy was a cross-account snapshot share encrypted with a KMS key in the DR account, and the principal performing the final decrypt was the RDS service-linked role, whose calls carry aws:PrincipalOrgID of neither org during part of the workflow. The BoolIfExists guard on aws:PrincipalIsAWSService was meant to let that through, but the KMS grant created by RDS evaluated with the key context absent, so the deny fired.

The fix was not to weaken the perimeter. They scoped the KMS portion of the RCP to also honor the grant-issuing service via kms:ViaService, keeping S3, SQS, and Secrets Manager fully locked:

{
  "Sid": "EnforceOrgPerimeterExceptKmsGrants",
  "Effect": "Deny",
  "Principal": "*",
  "Action": ["kms:Decrypt", "kms:CreateGrant", "kms:GenerateDataKey"],
  "Resource": "*",
  "Condition": {
    "StringNotEqualsIfExists": { "aws:PrincipalOrgID": "o-abcd1234ef" },
    "BoolIfExists": { "aws:PrincipalIsAWSService": "false" },
    "Null": { "kms:GranteePrincipal": "true" }
  }
}

Critically, they caught it only because the RCP was staged in a Sandbox OU that mirrored the DR topology — a throwaway “DR” account in the sandbox exercised the same snapshot-copy flow, so the production rollout never broke. The blast radius of the mistake was one sandbox account for one night, not 140 production accounts.

The incident also exposed a second, quieter gap. While debugging, the team ran aws securityhub get-findings from the Audit account and saw findings for us-east-1 but a suspicious silence from eu-west-1. The reason: when they had delegated Security Hub months earlier, they ran enable-organization-admin-account only in us-east-1, forgetting it is a regional call. Half their estate had no centralized Security Hub administration and nobody had noticed, because the management account’s own console looked fine. They fixed it with one extra call per region and added a check to their CI that asserts every security service is delegated in every operating region.

The two lessons compound: a perimeter RCP that looks airtight in review can break a legitimate service-mediated flow you never see in normal traffic — so stage it against a topology that mirrors production. And regional security services silently leave blind spots if you treat them like global ones — so make “delegated in every region” an automated assertion, not a memory.

Advantages and disadvantages

Preventive org-wide guardrails are powerful precisely because they are absolute and central — and that same absoluteness is what makes them dangerous. Weigh the model honestly:

Advantages (why guardrails win) Disadvantages (why they bite)
One policy enforces a rule across every account — no per-account drift to chase The blast radius of a mistake is the entire org; a bad root SCP can brick every member account at once
SCPs/RCPs never grant, so they cannot accidentally open access — adding one only ever subtracts Because they only subtract, they can silently break legitimate access you forgot about (service-linked roles, cross-account flows)
Preventive — the action is blocked before it touches the resource, not detected after Debugging an AccessDenied is harder: the cause may be three OUs up, invisible from the account
The management account is exempt, so you can always recover from the top That same exemption means tests from the management account lie — it looks healthy while members are broken
Declarative policies hold the baseline even across new service APIs and new accounts Declarative policies use unfamiliar @@assign syntax and a different mental model from IAM
RCPs build a data perimeter without editing thousands of resource policies RCPs cover only a specific service list — assume-everything is a trap
Conditions (PrincipalOrgID, tags, service keys) make denies surgical Bool vs BoolIfExists and the two service keys are easy to confuse — the #1 SCP bug

The model is right for any multi-account estate where universal, audit-provable controls matter — which is to say every regulated or mid-size-and-up AWS customer. It bites hardest the first time, on the team that skips staging, attaches a region lock without the iam/sts carve-out, and severs role assumption org-wide; or the team that protects a role with no exception clause and makes it permanently unmanageable. Every disadvantage here is manageable — but only through the staging discipline of Step 6, which is the entire reason that section exists.

Hands-on lab

Stand up the foundational guardrails in a non-production org (or a sandbox OU), prove a region lock actually blocks a real call, and tear it down. This is free — Organizations, SCPs, and RCPs carry no charge; you pay only if you leave billable resources running, which this lab does not create. Run from a principal in the management account for setup, but test from a member-account principal.

Safety first. Do this in a throwaway org or a Sandbox OU with no real workloads. A region lock without the global-service carve-out can sever role assumption — that is the whole point of testing it in a safe place.

Step 1 — Identify the org and enable policy types.

ROOT_ID=$(aws organizations list-roots --query 'Roots[0].Id' --output text)
ORG_ID=$(aws organizations describe-organization --query 'Organization.Id' --output text)
echo "Root: $ROOT_ID   Org: $ORG_ID"

aws organizations enable-policy-type --root-id "$ROOT_ID" --policy-type SERVICE_CONTROL_POLICY

Expected: the enable call returns the root with SERVICE_CONTROL_POLICY listed as ENABLED under PolicyTypes.

Step 2 — Create a Sandbox OU to stage into.

SANDBOX_OU=$(aws organizations create-organizational-unit \
  --parent-id "$ROOT_ID" --name Sandbox \
  --query 'OrganizationalUnit.Id' --output text)
echo "Sandbox OU: $SANDBOX_OU"

Step 3 — Author a region-lock SCP that carves out global services.

cat > scp-region-lock.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyOutsideApprovedRegions",
    "Effect": "Deny",
    "NotAction": ["iam:*","organizations:*","sts:*","cloudfront:*","route53:*","support:*"],
    "Resource": "*",
    "Condition": { "StringNotEquals": { "aws:RequestedRegion": ["us-east-1","eu-west-1"] } }
  }]
}
JSON

aws accessanalyzer validate-policy \
  --policy-document file://scp-region-lock.json \
  --policy-type SERVICE_CONTROL_POLICY

Expected: validate-policy returns an empty findings list (or only SUGGESTION-level notes) — no ERROR or SECURITY_WARNING.

Step 4 — Simulate the intended deny before attaching.

aws iam simulate-custom-policy \
  --policy-input-list file://scp-region-lock.json \
  --action-names "ec2:RunInstances" \
  --context-entries "ContextKeyName=aws:RequestedRegion,ContextKeyValues=ap-south-1,ContextKeyType=string" \
  --query 'EvaluationResults[0].EvalDecision'

Expected: "explicitDeny" — a RunInstances in ap-south-1 is blocked, exactly as designed.

Step 5 — Create and attach the SCP to the Sandbox OU.

SCP_ID=$(aws organizations create-policy \
  --name region-lock --type SERVICE_CONTROL_POLICY \
  --content file://scp-region-lock.json \
  --query 'Policy.PolicySummary.Id' --output text)

aws organizations attach-policy --policy-id "$SCP_ID" --target-id "$SANDBOX_OU"

Step 6 — Prove enforcement from a member account in the Sandbox OU. Assume a role into a sandbox member account, then attempt a blocked-region call:

# From inside a Sandbox member account (NOT the management account):
aws ec2 describe-availability-zones --region ap-south-1
# Expected: An error occurred (UnauthorizedOperation/AccessDenied) ... explicit deny

Expected: an AccessDenied/UnauthorizedOperation error citing the SCP — the region lock works. The same call in us-east-1 succeeds.

Step 7 — Confirm what actually applies and watch the deny in CloudTrail.

aws organizations list-policies-for-target \
  --target-id "$SANDBOX_OU" --filter SERVICE_CONTROL_POLICY --output table

Map of what each lab step proves:

Step What you did What it proves Real-world analogue
1 Enable SERVICE_CONTROL_POLICY Policy types are off by default First-day org setup
3 validate-policy the document Linting catches errors pre-attach CI gate on every policy PR
4 simulate-custom-policy The deny fires for the right context Dry-run before production rollout
6 AccessDenied from a member account The guardrail bites where it matters Watching a real workflow under the policy
7 list-policies-for-target Confirms inheritance/attachment The audit question “what applies here?”

Cleanup (no lingering charges, but tidy the org).

aws organizations detach-policy --policy-id "$SCP_ID" --target-id "$SANDBOX_OU"
aws organizations delete-policy --policy-id "$SCP_ID"
aws organizations delete-organizational-unit --organizational-unit-id "$SANDBOX_OU"

Cost note. Organizations, SCPs, and RCPs are free; this lab creates no billable resources, so the only “cost” is a few minutes. (If you instead spun up an EC2 instance to test, remember to terminate it — but the region lock above should have blocked that in the denied region.)

Common mistakes & troubleshooting

This is the playbook — the part you bookmark. First as a scannable table you read while a rollout is going sideways, then the entries that bite hardest expanded with the full confirm-and-fix detail. Note the discipline running through every row: confirm from inside a member account, never from the exempt management account.

# Symptom Root cause Confirm (exact cmd / path) Fix
1 After a region-lock SCP, role assumption / IAM calls fail org-wide NotAction omitted iam/sts; us-east-1 not allowed From a member account: aws sts get-caller-identity / any iam:* → explicit Deny Add iam,sts,organizations to NotAction; keep us-east-1 allowed
2 You wrote an Allow SCP to grant a service and it still doesn’t work SCPs never grant — the principal lacks an IAM Allow iam simulate-principal-policy shows implicit deny from IAM, not SCP Grant the action in an identity policy; SCP is only the ceiling
3 A protected security role can’t be modified by anyone, including its owner Role-protection SCP has no ArnNotLike exception iam simulate-principal-policy for the admin path → explicit Deny Add ArnNotLike carve-out for OrgPlatformAdmin; reapply
4 Workloads in one account have the wrong guardrails Account is in the wrong OU (or attached at account level) aws organizations list-parents --child-id <acct> Move the account to the correct OU; remove account-level attachments
5 A deny meant to exempt services breaks a nightly job / SLR flow Used Bool not BoolIfExists; key absent → deny fired CloudTrail AccessDenied with a service/SLR principal Switch to BoolIfExists; for KMS add kms:ViaService carve-out
6 An RCP blocks a legitimate cross-account or partner flow RCP perimeter too broad; forgot a trusted account/service CloudTrail AccessDenied on S3/KMS/etc from the partner principal Add aws:PrincipalAccount allow / kms:ViaService; re-stage
7 RCP “doesn’t protect” a resource type you expected That service isn’t in the RCP-supported list Check the action’s service against the supported list Use an SCP/resource policy; don’t rely on RCP off-list
8 GuardDuty/Security Hub findings missing in one region Delegated admin enabled in only one region aws securityhub get-administrator-account --region <r> empty Re-run enable-organization-admin-account per region
9 Tag-on-create SCP blocks legitimate creates Null aws:RequestTag/... true because tag passed differently CloudTrail request params show no RequestTag for that key Ensure clients pass --tag-specifications; align the key name
10 New service launches are all blocked for teams Allow-list SCP model (removed FullAWSAccess) list-policies-for-target shows no FullAWSAccess Switch to deny model; restore FullAWSAccess, deny only what you must
11 Policy fails to attach: size error SCP exceeds the 5 KB limit (whitespace counts) create-policy returns MalformedPolicyDocument/size error Minify; split into multiple policies; consolidate Sids
12 Everything looks fine from the console but members are broken You tested from the exempt management account Re-test the exact call from a member-account principal Always validate from inside a target OU
13 Can’t enable a policy type at all Org is consolidated-billing-only, not all-features describe-organization --query 'Organization.FeatureSet' Enable all features: enable-all-features (requires member consent)
14 An over-broad deny accidentally hits the org’s own automation Deny didn’t ArnNotLike the deployment/CI role CloudTrail AccessDenied for the pipeline role Add the automation role to the ArnNotLike exception

The expanded form for the entries that cause the most pain:

1. After attaching a region-lock SCP, role assumption or IAM calls fail across the whole org. Root cause: The NotAction carve-out omitted iam/sts/organizations, and your allowed-region set doesn’t include us-east-1 where those global services authenticate. Confirm: From a member-account principal, aws sts get-caller-identity or any iam:* call returns an explicit Deny; iam simulate-custom-policy against the SCP with the action reproduces it. Fix: Add iam:*, sts:*, organizations:* (plus route53, cloudfront, support) to NotAction, and keep us-east-1 in the allowed regions even for an EU-only org. Re-validate, re-simulate, then re-attach. This is the single most important policy to test from inside an account before promoting.

2. You wrote an SCP that “allows” a service to grant access, and it does nothing. Root cause: SCPs never grant; they only filter. An Allow SCP just declares the action is within the ceiling — the principal still needs an IAM Allow. Confirm: aws iam simulate-principal-policy for the principal shows the deny coming from the identity policy (implicit deny), not from any SCP. Fix: Add the permission to an identity (or resource) policy. Use SCPs only to subtract; if you want an allow-list model, remove FullAWSAccess and explicitly allow — but accept the brittleness.

3. A protected security/break-glass role becomes unmanageable by everyone. Root cause: The role-protection SCP denies IAM writes on the role with no ArnNotLike exception for the intended admin path — so even the platform-admin role is denied. Confirm: iam simulate-principal-policy for OrgPlatformAdmin against iam:UpdateRole on the protected ARN returns explicit Deny. Fix: Add "ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/OrgPlatformAdmin" } to the condition. Because the management account is exempt, your only escape without this is moving the account out of the org temporarily — so never ship the policy without the carve-out.

5. A service-mediated flow (service-linked role, FAS) breaks under a deny that was supposed to exempt services. Root cause: You used Bool instead of BoolIfExists on aws:ViaAWSService/aws:PrincipalIsAWSService; when the key is absent from the request context the plain Bool doesn’t behave as intended and the deny fires. Confirm: CloudTrail shows the AccessDenied with a service or service-linked-role principal; the same human call succeeds/fails as expected, isolating it to the service path. Fix: Switch to BoolIfExists. For KMS grants created by services (RDS, etc.), add a kms:ViaService carve-out or a Null kms:GranteePrincipal guard, as in the Meridian Pay scenario.

6. An RCP perimeter blocks a legitimate cross-account or partner flow. Root cause: The perimeter is too broad — it denies everything not in your org, but you have a sanctioned partner account or a service path you forgot. Confirm: CloudTrail AccessDenied on S3/KMS/SQS/Secrets Manager from the partner principal; the principal’s account isn’t your PrincipalOrgID. Fix: Add an allow for the specific trusted account (aws:PrincipalAccount) or service (aws:PrincipalIsAWSService, kms:ViaService), keeping everything else locked. Always stage RCPs against a topology that includes these flows.

8. GuardDuty or Security Hub findings are missing in one region. Root cause: These are regional services; you enabled delegated admin in one region and assumed it was global. Confirm: aws securityhub get-administrator-account --region <region> (or GuardDuty equivalent) returns empty in the un-delegated region. Fix: Run enable-organization-admin-account in every operating region, and add a CI assertion that every security service is delegated in every region you use.

Best practices

Security notes

The security controls and what each one defends, mapped to the mechanism:

Control Mechanism Defends against Residual risk to watch
Region lock (SCP) aws:RequestedRegion deny + global carve-out Data/resources sprawling to ungoverned regions The carve-out itself (global services unrestricted)
Root-user lockdown (SCP) aws:PrincipalArn ...:root deny Daily use of unscoped root credentials Genuinely root-only ops need a break-glass path
Audit-pipeline protection (SCP) Deny CloudTrail/Config disable + WORM bucket Attackers blinding detection Log-archive account compromise
Data perimeter (RCP) aws:PrincipalOrgID deny on the resource side External principals reaching your data Forgotten trusted partner/service flows
Org-membership fence (SCP) aws:PrincipalOrgID on share actions Snapshot/AMI exfiltration to foreign accounts Service-linked-role flows (needs ViaAWSService)
IMDSv2 + VPC BPA (declarative) @@assign EC2 state SSRF credential theft; accidental public subnets Pre-existing instances/VPCs (enforce on existing too)
Break-glass exemption ArnNotLike + offline creds Self-lockout from a bad deny Misuse if not tightly audited

Cost & sizing

The good news on cost: the controls themselves are free. AWS Organizations, SCPs, RCPs, and declarative policies carry no charge — you pay for the resources accounts create under them, not for the governance layer. The cost conversation here is about the supporting services the guardrails assume and the operational effort, not per-policy fees.

A rough monthly picture for a ~100-account org with centralized logging and detection (the guardrails are free; these are the services they rely on):

Cost driver What you pay for Rough INR / month Free? Watch-out
Organizations + SCP + RCP + declarative The governance layer itself ₹0 Yes Nothing — it’s free
Org CloudTrail (mgmt events) First copy of management events ₹0 Yes (1st copy) Data events are extra and can spike
CloudTrail S3 storage Log-archive bucket (WORM + lifecycle) ~₹4,000–15,000 No Grows with retention; tier to Glacier
CloudTrail data events (optional) Per-event logging on S3/Lambda ~₹0–50,000+ No Enable selectively — busy buckets are pricey
AWS Config (org-wide) Config items + rule evaluations ~₹15,000–60,000 No Scope resource types; the silent big one
GuardDuty (org) Events/flow-logs analyzed ~₹10,000–40,000 No (30-day trial) Scales with traffic/account count
Security Hub (org) Finding ingestion + checks ~₹5,000–25,000 No Per-region; per-check costs add up

Sizing guidance: the only “sizing” knob on the guardrail layer is the 5 KB per-SCP limit — consolidate related Deny statements into one policy, minify in CI, and split across multiple policies when you outgrow it (there’s a limit on policies attached per entity too, so don’t fragment needlessly). For the supporting services, the lever is scope: record in Config only the resource types you govern, log data events only where you need them, and enable detection plans selectively. The governance is free; discipline keeps the services it relies on affordable.

Interview & exam questions

1. Does an SCP grant permissions? Explain the evaluation rule. No — an SCP never grants. It filters the maximum permissions of principals in an account. The effective permission is the intersection of the identity policy, every SCP, and every RCP from root to account; an explicit Deny in any SCP wins, and an Allow SCP does nothing unless an identity policy also allows the action. Maps to SAP-C02 (organizational complexity) and SCS-C02 (governance).

2. Why is the management account exempt from SCPs, and what’s the implication? So you can never brick your own org from the top — there’s always one account from which to recover. The implication is twofold: never run workloads in the management account (they’re unguardrailed), and never test guardrails from it (it looks healthy while members are broken). Always validate from a member-account principal.

3. You attach a region-restriction SCP and role assumption breaks org-wide. What happened and how do you fix it? The NotAction carve-out omitted iam/sts (and likely organizations), and your allowed regions didn’t include us-east-1 where those global services authenticate — so STS calls were denied. Fix by adding iam:*, sts:*, organizations:* to NotAction and keeping us-east-1 allowed even in an EU-only org. Test this policy from inside a member account before promoting.

4. What’s the difference between an SCP and an RCP? An SCP filters the identity side (what your principals can do); an RCP filters the resource side (caps every resource policy’s grants, stopping external principals from reaching your resources). RCPs cover a specific service list (S3, STS, KMS, SQS, Secrets Manager, ECR, OpenSearch Serverless); SCPs apply to all services. Both deny-only, both exempt the management account. RCPs build the data perimeter SCPs can’t.

5. When do you use aws:ViaAWSService vs aws:PrincipalIsAWSService, and why BoolIfExists? aws:PrincipalIsAWSService is true when a service principal calls directly (CloudTrail writing to S3); aws:ViaAWSService is true when a service uses your credentials in a forward access session (Athena re-driving S3 reads). Use BoolIfExists because if the key is absent from the request context, plain Bool mis-evaluates — IfExists treats absence as “called directly,” so the deny applies as intended.

6. How do you build an org-wide data perimeter, and what’s its main risk? Attach an RCP that denies access to in-scope resources unless aws:PrincipalOrgID matches your org or the caller is an AWS service (aws:PrincipalIsAWSService). It can only subtract, so it can’t open a resource — but it can silently break a legitimate cross-account/partner or service-linked-role flow you forgot. Stage it against a topology that mirrors production and watch CloudTrail for AccessDenied.

7. What are declarative policies and how do they differ from SCPs? Declarative policies pin a service’s configuration state (EC2 first: IMDSv2, VPC Block Public Access, snapshot/AMI public-access blocks) rather than filtering API calls, using @@assign syntax. AWS guarantees the baseline holds even as the service ships new APIs and new accounts join — unlike an SCP, which denies by action and needs updating when new actions appear.

8. How do you delegate GuardDuty and Security Hub, and what’s the gotcha? Via each service’s own enable-organization-admin-account API, naming the Audit/Security account — and the gotcha is that they’re regional, so you must repeat the call in every operating region. Most other services (Access Analyzer, Config, StackSets) delegate once via the Organizations register-delegated-administrator. Mixing these up leaves regional blind spots.

9. Why is an account-level SCP attachment an anti-pattern? Because it doesn’t survive an account move — reorganize the OU tree and the account silently loses (or keeps) the wrong guardrails — and it’s invisible to audits that look at OU policy. Attach to OUs and let account placement do the work; reserve account-level attachments for documented, temporary exceptions.

10. How do you roll out a new guardrail without locking yourself out? Stage in a Sandbox OU of throwaway accounts that mirrors production, lint with validate-policy and dry-run with simulate-custom-policy, then promote OU-by-OU least-critical-first (Sandbox → NonProd → Infrastructure → Prod → Security last). Keep one break-glass role exempt via ArnNotLike with offline credentials, and confirm it still works after every change. Never test from the management account.

11. The 5 KB SCP size limit is blocking an attach. What do you do? Minify the JSON (whitespace counts toward the limit), consolidate related Deny statements under shared Sids/Action lists, and split into multiple policies if needed — being mindful of the per-entity attached-policy limit so you don’t over-fragment. Keep the source readable in Git and minify in the CI step that deploys it.

12. A protected role can’t be modified by anyone, including its owner. Why, and what’s the fix? The role-protection SCP denies IAM writes on the role with no exception clause, so even the intended admin is denied — and since the management account is exempt, your only escape is moving the account out of the org. Fix by adding an ArnNotLike carve-out for the platform-admin/break-glass principal. Always leave one authenticated path.

These map primarily to AWS Certified Security – Specialty (SCS-C02) — governance, data perimeters, and detective-control protection — and AWS Certified Solutions Architect – Professional (SAP-C02) — designing for organizational complexity, multi-account governance, and account structure. A compact cert mapping for revision:

Question theme Primary cert Exam objective area
SCP evaluation / intersection rule SCS-C02 / SAP-C02 Governance; multi-account access control
Region lock + global carve-out SAP-C02 Designing org-wide guardrails
RCP data perimeter SCS-C02 Data perimeters; resource-side controls
ViaAWSService vs PrincipalIsAWSService SCS-C02 Advanced policy conditions
Declarative policies (IMDSv2, VPC BPA) SCS-C02 Preventive configuration baselines
Delegated administration (regional gotcha) SCS-C02 / SAP-C02 Centralized security operations
Safe rollout / break-glass SAP-C02 Operational excellence; avoiding lockout

Quick check

  1. You write an SCP with an Allow statement to give an account access to a new service, but the principals still get AccessDenied. Why does the Allow SCP not work?
  2. A region-restriction SCP is attached at the root and suddenly every account fails sts:AssumeRole. What did the policy almost certainly get wrong, and what’s the fix?
  3. True or false: an RCP can protect any AWS resource type from external access.
  4. You need a deny to apply to humans but not to a service-linked role acting on a user’s behalf. Which condition key do you use, and why BoolIfExists instead of Bool?
  5. You delegated Security Hub administration to the Audit account in us-east-1, but eu-west-1 shows no centralized findings. What’s the cause and the fix?

Answers

  1. Because SCPs never grant — they only filter the maximum permissions. An Allow SCP merely keeps the action within the ceiling; the principal still needs an Allow in an identity policy. Add the permission to an IAM policy; the SCP is the ceiling, not the floor.
  2. It almost certainly omitted sts (and iam) from the NotAction carve-out while not allowing us-east-1, where STS authenticates globally — so role assumption was denied org-wide. Fix by adding iam:*, sts:*, organizations:* to NotAction and keeping us-east-1 in the allowed regions even for a non-US org. Test it from inside a member account before promoting.
  3. False. RCPs cover only a specific, growing service list (at launch S3, STS, KMS, SQS, Secrets Manager; later ECR and OpenSearch Serverless). For resource types outside that list, RCPs do nothing — use an SCP or the resource’s own policy.
  4. Use aws:ViaAWSService (true when a service makes the call using the user’s credentials) and exempt it ("BoolIfExists": { "aws:ViaAWSService": "false" }). Use BoolIfExists because if the key is absent from the request context, a plain Bool mis-evaluates; IfExists treats absence as “the principal called directly,” so the deny still applies to humans.
  5. GuardDuty/Security Hub are regional services, and enable-organization-admin-account was run only in us-east-1. Fix by running the delegation call in every operating region (eu-west-1 here), and add a CI assertion that every security service is delegated in every region you use.

Glossary

Next steps

You can now design, write, and safely roll out org-wide guardrails and delegate security services without locking yourself out. Build outward:

AWSOrganizationsSCPRCPGovernanceSecurityDelegated AdminData Perimeter
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments