expert 50 min read aws Updated: 2026-09-14

AWS Resource Control Policies & Data Perimeter Design

Design and enforce an organization-wide data perimeter with Resource Control Policies. Covers how RCPs differ from SCPs, the 45 supported services, quotas and exemptions, condition-key patterns, safe rollout across an OU hierarchy, and diagnosing RCP denials.

๐Ÿ“‹ Prerequisites

  • Working knowledge of AWS Organizations, OU hierarchies, and Service Control Policies.
  • Fluency in IAM policy evaluation logic โ€” identity-based, resource-based, and permissions boundaries.
  • An organization with all features enabled. RCPs are unavailable in consolidated-billing-only organizations.
  • Terraform or CloudFormation experience for deploying organization policies as code.
  • Access to CloudTrail across member accounts for pre-deployment impact analysis.

๐Ÿ’ก The Gap RCPs Were Built to Close

For a decade, the only organization-wide preventative control AWS offered was the Service Control Policy โ€” and an SCP can only constrain principals that live inside your accounts. It has nothing to say about a principal in someone else's account calling into your S3 bucket, or about a resource-based policy that a developer widened last Tuesday. Resource control policies close that gap by bounding what can be done to your resources, no matter who is asking. This guide covers what RCPs actually enforce, where they stop, and how to build a data perimeter with them without locking yourself out of production.

๐Ÿท๏ธ Topics Covered

aws resource control policiesrcp vs scp differenceaws data perimeter designaws:ResourceOrgID condition keyaws:PrincipalOrgID data perimeterprevent data exfiltration awsaws confused deputy preventionrcp rollout strategy organizationsvpc endpoint policy data perimeteraws organizations rcp examples

Why "SCPs for Resources" Is the Wrong Mental Model

Nearly every introduction to RCPs opens with some version of "they're SCPs, but for resources." It is a useful one-line orientation and a bad foundation for policy design, because the two policy types differ in four ways that change how you write them.

1. They Point in Opposite Directions

An SCP bounds what principals in your member accounts can do โ€” anywhere, to anything. An RCP bounds what anyone can do to resources in your member accounts. The AWS Organizations documentation frames the choice exactly this way: use an SCP to limit IAM principals within your organization's member accounts; use an RCP to restrict principals external to your organization making requests against resources inside it.

The practical consequence is that RCPs reach principals an SCP can never touch. An RCP evaluates on requests from principals outside your organization, and it applies to the account root user. The one class of caller it does not reach is service-linked roles, covered below.

๐Ÿ“ The Same Control, Two Directions

SCP  โ”€โ”€  "principals in MY accounts may not call s3:PutObject
          against buckets outside my organization"
          โ†’ stops MY people writing data OUT (exfiltration by my identities)

RCP  โ”€โ”€  "no principal may call s3:GetObject against MY buckets
          unless they belong to my organization"
          โ†’ stops OTHER people reading data OUT (exfiltration to their identities)

Neither substitutes for the other. A complete perimeter needs both,
which is why the AWS data perimeter guidance pairs them.

2. Inheritance Is a Union of Denies, Not an Intersection of Allows

This is the difference that most often produces surprising results, and it is worth being precise about.

With SCPs, a permission survives only if every policy in the path from the root down to the account allows it. Attaching an SCP that allows less at any level narrows the effective set. Practitioners internalise this as "SCPs intersect."

RCPs do not work that way. For customer-authored RCPs the Effect element must be Deny โ€” Allow is supported only in the AWS managed RCPFullAWSAccess policy. Evaluation is therefore a union: a permission is denied for a resource if any RCP from the root, through each OU in the direct path, down to the account itself denies it. There is no allow-list to intersect, because you cannot write an allow.

๐Ÿ”’ RCPFullAWSAccess and Why You Cannot Detach It

When you enable RCPs, AWS attaches this managed policy to the organization root, every OU, and every account. It cannot be detached, and it counts against your per-entity policy quota.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RCPFullAWSAccess",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "*",
      "Resource": "*"
    }
  ]
}

Read this carefully, because it is widely misread. This policy does not grant anything. Its function is to let all principals and actions pass through RCP evaluation, so that enabling RCPs is a no-op until you attach your first Deny. Permissions still come entirely from identity-based and resource-based policies. The AWS documentation is explicit that no permissions are granted by an RCP.

3. The Syntax Is Resource-Based, and Deliberately Constrained

RCPs use resource-based policy syntax, which means a Principal element is mandatory in every statement. It also means several elements you reach for out of habit are simply unavailable.

โš ๏ธ Syntax Constraints That Bite on the First Policy

REQUIRED in every statement:  Effect, Principal, Action, Resource (or NotResource)

Effect      must be "Deny" in any policy you write.
            "Allow" exists only in RCPFullAWSAccess.

Principal   mandatory, and the ONLY permitted value is "*".
            You scope to specific principals via the Condition element,
            never via Principal.

Action      must name a service prefix. "Action": "*" is not permitted
            in a customer managed RCP. Wildcards within a service are fine:
            "s3:*", "sts:Get*", "kms:Decrypt".

NOT SUPPORTED:   NotPrincipal        NotAction

Version     must be "2012-10-17".

The Principal: "*" plus condition-based scoping pattern is the single biggest stylistic departure from SCP authoring. If you find yourself trying to name a principal in the Principal block, you are writing a bucket policy, not an RCP.

4. Effective Permissions Are an Intersection Across Four Policy Types

AWS states the model directly: effective permissions are the logical intersection of what the RCPs and SCPs allow and what the identity-based and resource-based policies allow. In operational terms, an RCP can only ever take permissions away. A resource owner who attaches a resource-based policy granting full access to the world still gets nothing if an RCP above their account denies it โ€” and equally, an RCP that denies nothing grants nothing.

๐ŸŽฏ What This Changes About How You Write Them

  • Design for the union. Because any level can deny, an RCP at the root is genuinely organization-wide with no per-OU escape hatch. Exceptions must be written into the policy's conditions, not layered underneath it.
  • Budget your denies. There is no allow-list to relax later, so an over-broad deny is corrected by editing the policy, not by attaching a narrower one below.
  • Condition keys carry all the nuance. With Principal pinned to "*" and NotAction unavailable, every exception you need lives in the Condition block.
  • Never rely on an RCP to grant. An RCP is a ceiling. Removing an identity policy and expecting the RCP to keep access working is a category error.

Which Services RCPs Actually Cover

If you read a blog post about RCPs written any time in the year after launch, it told you they support five services: S3, STS, KMS, SQS, and Secrets Manager. That was accurate at the November 2024 launch. It is badly out of date now, and it is the most common factual error in current RCP writing.

At the time of writing, the AWS Organizations documentation lists 45 services. The set includes several that people still assume are out of scope โ€” DynamoDB and DynamoDB Accelerator among them, which directly contradicts the widely-cited claim that RCPs cannot cover AWS databases.

๐Ÿ“‹ Current Supported Service Prefixes

appconfig            cognito-identity     kendra               s3
appstream            cognito-idp          kinesisvideo         secretsmanager
autoscaling          comprehend           kms                  signin
clouddirectory       comprehendmedical    logs                 sqs
cloudfront           cost-optimization-hub memorydb            sts
cloudsearch          dax                  networkmonitor       support
codebuild            dynamodb             opensearch           textract
codecommit           ecr                  aoss                 timestream-influxdb
codepipeline         events               pca-connector-ad     transcribe
                     firehose             polly                transfer
                     fis                  pricing              translate
                     health               inspector-scan       wafv2

This list has grown steadily โ€” ECR and OpenSearch Serverless were added in June 2025, Cognito and CloudWatch Logs in January 2026, with further expansions in between. Treat the AWS Organizations documentation page as the source of truth and re-check it before you scope a policy; anything written down here, including this guide, has a shelf life.

Supported Service Does Not Mean Supported Action

This is the nuance that catches people who checked the service list and stopped there. An RCP applies to actions that authorize a resource as part of the request. AWS defines this precisely: the resources RCPs apply to are those appearing in the "Resource type" column of the action table in the Service Authorization Reference. If an action has an entry in that column, the RCP attached to the account owning the resource is evaluated.

So s3:GetObject authorizes the object resource and is in scope. An action in a supported service that authorizes no resource type is not covered by an RCP, even though its service prefix appears in the list above. Before you rely on a deny statement, confirm the specific action authorizes a resource.

๐Ÿ” Verifying an Action Is In Scope

1. Open the Service Authorization Reference for the service:
   https://docs.aws.amazon.com/service-authorization/latest/reference/
     list_<service>.html

2. Find the action in the Actions table.

3. Check the "Resource types" column.
   Populated  -> the action authorizes a resource; an RCP applies.
   Empty      -> no resource is authorized; the RCP will not fire.

4. Confirm the resource is owned by a MEMBER account in your org.
   RCPs apply to resources owned by accounts in the organization that
   attached them. They do not reach resources in external accounts,
   even when your principals are the ones calling.

That last point is worth restating because it is the mirror image of what most people expect. Consider a bucket owned by Account A inside your organization, with a bucket policy granting access to a user in Account B outside it. Your RCP applies to that bucket when Account B's user calls it. But your RCP does not apply to resources in Account B when your own users call them. Protecting against your principals reaching outside is SCP territory.

Limits, Exemptions, and the Gaps They Leave

RCP quotas are meaningfully tighter than SCP quotas, and the exemption list is longer than most teams realise. Both shape the architecture, so they belong at the design stage rather than the debugging stage.

Quotas

๐Ÿ“ RCP Quotas Against SCP Quotas

                                    RCP        SCP
Maximum policy document size        5,120      10,240   characters
Maximum attached to root              5           10
Maximum attached per OU               5           10
Maximum attached per account          5           10
Maximum policies in the organization  2,000      10,000

The RCPFullAWSAccess managed policy counts against the 5.
Your usable budget is therefore FOUR customer RCPs per entity.

Inherited policies do not count against the per-entity limit โ€”
only policies directly attached to that root, OU, or account.

Half the character budget and half the attachment slots of an SCP is a real constraint at scale. A perimeter built as one policy per control objective will hit the four-policy ceiling quickly, which pushes you toward grouping several statements into fewer, larger documents โ€” right into the 5,120-character wall.

โš ๏ธ The Whitespace Trap in Infrastructure as Code

  • Symptom: a policy that saves cleanly in the console fails with a size error when the same JSON is applied through Terraform or the CLI.
  • Cause: AWS documents that saving through the console strips extra whitespace between JSON elements and outside quotation marks, and does not count it. Saving through an SDK operation or the CLI stores the policy exactly as provided, with no automatic removal.
  • Consequence: a pretty-printed policy read with file("perimeter.json") can spend hundreds of its 5,120 characters on indentation.
  • Fix: build the policy with Terraform's jsonencode(), which emits compact JSON, or with aws_iam_policy_document. Reserve file() for policies you have already minified, and assert the length in CI.

What RCPs Cannot Reach

Five exemptions, each with an architectural consequence.

๐Ÿšซ The Exemption List

1. Resources in the MANAGEMENT ACCOUNT
   RCPs affect member accounts only. Delegated administrator accounts
   are member accounts, so they ARE covered.

2. SERVICE-LINKED ROLES
   RCPs do not affect the effective permissions of any service-linked
   role, and do not affect an AWS service's ability to assume one โ€”
   the SLR trust policy is out of scope too.

3. AWS MANAGED KMS KEYS
   Created and managed by AWS on your behalf; their permissions cannot
   be changed or constrained by an RCP.

4. kms:RetireGrant
   Explicitly listed as unaffected. Grant retirement is authorised
   through a separate path.

5. Anything a permission was never granted for
   An RCP does not grant. A principal with no identity or resource
   policy has no access regardless of what the RCP permits.

The management account exemption is the one with teeth. If your organization's management account holds workload resources โ€” an S3 bucket someone stood up years ago, a KMS key still in use โ€” those resources sit permanently outside every perimeter control you build. This is an independent argument for the long-standing AWS guidance to keep workloads out of the management account entirely, and it is worth auditing before you design the perimeter rather than discovering it afterwards.

The service-linked role exemption matters differently. It means an RCP cannot be used to constrain what an AWS service does on your behalf through its SLR. That is usually what you want โ€” it is why a well-scoped RCP does not break AWS Config, GuardDuty, or Backup โ€” but it also means an SLR is not a control point you can tighten from Organizations.

The Data Perimeter Model

A data perimeter is not a product and not a single policy. It is a set of coarse-grained guardrails that hold three invariants true across every account you own: only trusted identities reach your resources, your identities reach only trusted resources, and both happen only over expected networks. RCPs implement one column of that grid. Understanding which column keeps you from trying to make RCPs do work they structurally cannot.

๐Ÿงญ Three Perimeters, Three Enforcement Points

                    MY IDENTITIES        MY RESOURCES         MY NETWORKS
                    ---------------      ---------------      ---------------
Identity            (n/a)                RCP                  VPC endpoint
perimeter                                aws:PrincipalOrgID   policy
"only trusted                            aws:PrincipalIsAWS   aws:PrincipalOrgID
 identities"                              Service

Resource            SCP                  (n/a)                VPC endpoint
perimeter           aws:ResourceOrgID                         policy
"only trusted                                                 aws:ResourceOrgID
 resources"

Network             SCP                  RCP                  (n/a)
perimeter           aws:SourceIp         aws:SourceVpc
"only expected      aws:SourceVpc        aws:VpceOrgID
 networks"          aws:SourceVpce       aws:ViaAWSService

RCPs own the middle column. They answer "who may touch my resources, and from where." They cannot answer "what may my principals touch" โ€” that is the SCP column, and it is why the AWS data perimeter guidance ships SCP examples alongside the RCP examples rather than instead of them.

The Reference Policy Set

AWS maintains a public repository of data perimeter policy examples. The RCP folder is organised into three policies, and the split is worth adopting because it maps onto how the controls fail and get exempted independently.

๐Ÿ“ฆ How to Split the Policies

  • Identity perimeter RCP. Denies access from principals outside your organization, with carve-outs for AWS service principals and named third-party accounts. Contains the confused-deputy statement.
  • Network perimeter RCP. Denies access that does not originate from your VPCs, your corporate CIDRs, or an AWS service acting on your behalf. Two variants exist depending on whether the services you use support aws:VpceOrgID.
  • Governance RCP. Protects the dependencies the other two rely on โ€” principally the session tags used to scope them. Without this, a trusted third party can tag its own sessions to exempt itself from your network perimeter.

Three policies plus RCPFullAWSAccess is exactly four of your five per-entity slots. That is not a coincidence in the reference design, and it is the practical reason to resist splitting further.

Writing the Core Perimeter RCPs

The policies below are written from the AWS reference patterns and the condition-key semantics documented in the IAM User Guide. Treat them as annotated starting points. Every one of them will deny something you need on the first attempt in a real organization, which is what the rollout section is for.

The Identity Perimeter

The core statement denies any action against your resources when the calling principal is not in your organization and is not an AWS service.

๐Ÿ” EnforceOrgIdentities

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceOrgIdentities",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:*",
        "sqs:*",
        "kms:*",
        "secretsmanager:*",
        "sts:AssumeRole",
        "ecr:*",
        "dynamodb:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:PrincipalOrgID": "o-EXAMPLE12345",
          "aws:PrincipalAccount": [
            "111122223333",
            "444455556666"
          ]
        },
        "BoolIfExists": {
          "aws:PrincipalIsAWSService": "false"
        }
      }
    }
  ]
}

Four things in that statement earn their place, and three of them are the reason hand-rolled versions break.

  • StringNotEqualsIfExists rather than StringNotEquals. If the condition key is absent from the request context, StringNotEquals evaluates true and the deny fires. The IfExists suffix makes the statement skip requests where the key was never populated, which is the difference between a perimeter and an outage.
  • aws:PrincipalIsAWSService instead of NotPrincipal. RCPs do not support NotPrincipal, and AWS documents that you cannot use NotPrincipal with a service principal in any case. This condition key is the supported way to exempt AWS services from a Deny.
  • aws:PrincipalAccount for named third parties. Vendors with legitimate cross-account access โ€” a backup provider, a CSPM scanner โ€” go here by account ID. Keep the list short and reviewed; every entry is a hole in the perimeter.
  • An explicit action list, not a wildcard. "Action": "*" is invalid in a customer RCP. Enumerate service prefixes, and only ones that appear on the supported-services list.

โš ๏ธ Why STS Actions Are Enumerated, Not Wildcarded

The AWS reference policy deliberately omits several STS actions from this statement, and the reasoning is not obvious.

  • sts:AssumeRoleWithSAML and sts:AssumeRoleWithWebIdentity are excluded. The aws:PrincipalOrgID key is only present in the request context when the calling principal is a member of an organization. Federated users are not, so including these actions would deny every SAML and OIDC federation in your estate.
  • sts:SetSourceIdentity and sts:TagSession are excluded to avoid breaking the federation calls above when they set a source identity or pass session tags.
  • sts:GetCallerIdentity is excluded because it requires no permissions at all, so denying it accomplishes nothing.

Federated identity is instead constrained by separate statements that pin the trusted OIDC provider and, for multi-tenant providers such as GitHub Actions, the specific tenant within it via the sub claim.

Confused Deputy Protection

The identity statement lets every AWS service principal through. That is necessary โ€” CloudTrail, VPC Flow Logs, and Config all write to your buckets using service principals โ€” but on its own it means any AWS service can be induced to act against your resources on someone else's behalf. The second statement closes that.

๐ŸŽญ EnforceConfusedDeputyProtection

{
  "Sid": "EnforceConfusedDeputyProtection",
  "Effect": "Deny",
  "Principal": "*",
  "Action": [
    "s3:*",
    "sqs:*",
    "kms:*",
    "secretsmanager:*",
    "ecr:*"
  ],
  "Resource": "*",
  "Condition": {
    "StringNotEqualsIfExists": {
      "aws:SourceOrgID": "o-EXAMPLE12345",
      "aws:SourceAccount": [
        "111122223333",
        "444455556666"
      ]
    },
    "Bool": {
      "aws:PrincipalIsAWSService": "true"
    },
    "Null": {
      "aws:SourceAccount": "false"
    }
  }
}

Read this as: when an AWS service principal is calling, and the request carries a source account, that source must belong to my organization.

The Null operator on aws:SourceAccount is the load-bearing element and the part most often dropped. It scopes the statement to only those requests that actually populate the cross-service confused-deputy keys. AWS documents the choice explicitly: aws:SourceAccount is used in the Null check rather than aws:SourceOrgID so that the control still applies when a request originates from an account that does not belong to any organization. A pleasant side effect is that when a service adds support for these keys, it comes under the policy automatically.

Several integrations legitimately do not populate aws:SourceOrgID, and the Null condition is what stops them breaking. AWS names the cases: services that assume service roles you created, where iam:PassRole already enforces same-account placement; services using KMS grants, where the grant's encryption context restricts it to the resource it was created for; and Classic and Application Load Balancer access logging in some Regions, which uses service principals without populating the key.

The Network Perimeter

The network statement asserts that requests to your resources arrive over networks you control. Which condition key you reach for depends on service support.

๐ŸŒ EnforceNetworkPerimeter

{
  "Sid": "EnforceNetworkPerimeter",
  "Effect": "Deny",
  "Principal": "*",
  "Action": ["s3:*", "sqs:*", "kms:*", "secretsmanager:*"],
  "Resource": "*",
  "Condition": {
    "StringNotEqualsIfExists": {
      "aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"],
      "aws:VpceOrgID": "o-EXAMPLE12345",
      "aws:PrincipalAccount": ["111122223333"]
    },
    "BoolIfExists": {
      "aws:PrincipalIsAWSService": "false",
      "aws:ViaAWSService": "false"
    },
    "ArnNotLikeIfExists": {
      "aws:PrincipalArn": "arn:aws:iam::*:role/aws:ec2-infrastructure"
    }
  }
}
  • aws:VpceOrgID matches any VPC endpoint in your organization in one key, which is far more maintainable than enumerating endpoint IDs. Check the supported-services list for this key before relying on it.
  • aws:SourceVpc is the fallback for services that do not support aws:VpceOrgID โ€” but VPC IDs are only unique within a Region, so AWS pairs it with aws:RequestedRegion to prevent a VPC ID collision in another Region from satisfying the condition. If you use aws:SourceVpc, use both.
  • aws:ViaAWSService exempts forward access sessions: the pattern where you call service A, and service A then calls service B using your credentials. Athena reading from S3 on your behalf, or S3 calling KMS for SSE-KMS on upload, both land here. Omit this and you will break SSE-KMS.
  • The aws:ec2-infrastructure role ARN is how EC2 decrypts encrypted EBS volumes โ€” it calls KMS from the service network under a role created in your account. It needs an explicit exemption.

๐Ÿ’ฅ Two Things This Policy Will Break on Day One

  • CloudFront origin access identity. If any distribution still uses OAI to reach an S3 origin, the identity perimeter statement severs it. The correct fix is migrating to origin access control. If you need an interim bridge, exempt the OAI user ARN โ€” arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <id> โ€” via aws:PrincipalArn, and treat it as debt with a date on it.
  • ELB access logging. In some Regions, Classic and Application Load Balancers publish logs to your buckets using AWS account credentials rather than a service principal, so aws:PrincipalIsAWSService does not exempt them. Add the regional ELB account ID to aws:PrincipalAccount. Scope the bucket grant to the path containing the account ID, since the log object names always carry it.

Scoping and Exceptions

With NotAction unavailable and Principal pinned to "*", every exception lives in conditions or in NotResource. Four mechanisms, in rough order of preference:

๐Ÿท๏ธ Exempting Specific Resources

aws:ResourceTag/<key>     Tag-based exclusion. Cleanest, but only works
                          for resources supporting tag-based authorization.
                          Check the Service Authorization Reference.

s3:ExistingObjectTag      Service-specific equivalents where the generic
                          key is unsupported.

aws:ResourceAccount       Exclude everything owned by named accounts.
aws:ResourceOrgPaths      Exclude an entire OU subtree.

NotResource               Exclude specific ARNs outright. Blunt, verbose,
                          and expensive against a 5,120-character budget.
                          Use last.

If you scope the perimeter by tags on principals โ€” the reference policies use a dp:include:network convention โ€” you must also prevent untrusted parties setting those tags themselves. That is the entire purpose of the governance RCP: a statement blocking third parties from passing data-perimeter session tags on sts:AssumeRole. Without it, an account outside your organization can tag its own identities to opt out of your network perimeter.

Rolling Out Safely Across an OU Hierarchy

AWS states this plainly and it is worth repeating: do not attach RCPs to the organization root without thoroughly testing the impact first. The recommended progression is individual test accounts, then OUs lower in the hierarchy, then upward. What that guidance does not tell you is how to know the impact before you attach anything, which is the part worth engineering.

Phase 1 โ€” Measure Before You Write

Two data sources tell you what a perimeter would break, and both are available before you create a single policy.

๐Ÿ“Š CloudTrail as a Pre-Deployment Simulator

-- Athena over an organization CloudTrail trail.
-- Every request to your resources from a principal outside the org:
-- these are exactly the calls the identity perimeter would deny.

SELECT
  useridentity.type            AS principal_type,
  useridentity.accountid       AS calling_account,
  eventsource,
  eventname,
  count(*)                     AS calls
FROM cloudtrail_logs
WHERE eventtime >= '2026-06-01'
  AND recipientaccountid <> useridentity.accountid
  AND useridentity.accountid NOT IN (SELECT account_id FROM my_org_accounts)
  AND eventsource IN (
    's3.amazonaws.com','kms.amazonaws.com','sqs.amazonaws.com',
    'secretsmanager.amazonaws.com','sts.amazonaws.com','ecr.amazonaws.com'
  )
GROUP BY 1,2,3,4
ORDER BY calls DESC;

-- Ninety days is the minimum useful window. Quarterly batch jobs,
-- annual audit tooling, and DR tests will not appear in thirty.

Every row is either a third party you must allow-list or an access path you did not know existed. Both are findings. AWS additionally recommends reviewing IAM Access Analyzer external access findings alongside CloudTrail, since those surface resources that are currently shared externally or public whether or not anyone has called them recently. CloudTrail shows you what happened; Access Analyzer shows you what could.

Phase 2 โ€” Stage Through the Hierarchy

๐Ÿชœ Attachment Order

1. Sandbox account            Attach. Break things deliberately.
                              Verify every deny you expect actually fires.

2. Non-production OU          Attach. Run the full CI/CD suite,
                              cross-account deploys, and a DR drill.
                              Watch CloudTrail AccessDenied volume daily.

3. One production OU          Attach to your lowest-criticality prod OU.
                              Hold for a full business cycle โ€” month-end
                              close and batch windows are where the
                              surprises live.

4. Remaining production OUs   One at a time.

5. Organization root          Only after every OU below it has run the
                              policy clean. At this point attaching at
                              root is a consolidation, not a change.

Rollback at any stage is DetachPolicy, which takes effect immediately.
Keep the detach command in the runbook and make sure the on-call
engineer has permissions to run it from the management account.

One structural note on this sequence. Because RCP inheritance is a union of denies, moving a policy up the hierarchy never relaxes anything โ€” an RCP at the root denies for every account beneath it, and no OU-level policy can carve an exception out. This is the opposite of the SCP intuition, where a permissive parent leaves room for a restrictive child. Plan for exceptions to live inside the policy conditions from the start, because you cannot add them underneath later.

Phase 3 โ€” Watch the Right Signal

An RCP denial appears in CloudTrail as an AccessDenied error like any other. Alert on the rate of change rather than the absolute count โ€” most organizations have a persistent background level of denied calls, and what matters is the step change when a policy attaches.

๐Ÿ” Break-Glass Considerations

  • The management account is your escape hatch, by design. RCPs do not affect resources in it, and only it can detach a policy. Guard access to it accordingly.
  • RCPs apply to the root user of member accounts. A break-glass procedure that assumes root can bypass organization controls is wrong for RCPs. Test it.
  • Delegated administrator accounts are member accounts. Your security tooling account gets no exemption. If your Security Hub or Config delegated admin needs cross-account reach, verify it under the policy before rollout, not after.
  • Service-linked roles are exempt. This is what keeps AWS-native services working, and it is also why you should not assume an RCP constrains everything an AWS service does in your accounts.

Deploying and Validating RCPs as Code

An organization policy applied by hand in the console is an outage waiting for a Friday. RCPs deploy through the same Organizations API as SCPs, which means Terraform, CloudFormation, and CDK all handle them โ€” with one size trap specific to RCPs.

๐Ÿ—๏ธ Terraform

resource "aws_organizations_policy" "identity_perimeter" {
  name        = "identity-perimeter-rcp"
  description = "Denies access from principals outside the organization"
  type        = "RESOURCE_CONTROL_POLICY"

  # jsonencode emits compact JSON. Never use file() with a
  # pretty-printed document: the CLI and SDK store the policy
  # verbatim, so indentation counts against the 5,120 limit.
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "EnforceOrgIdentities"
        Effect    = "Deny"
        Principal = "*"
        Action    = ["s3:*", "sqs:*", "kms:*", "secretsmanager:*"]
        Resource  = "*"
        Condition = {
          StringNotEqualsIfExists = {
            "aws:PrincipalOrgID" = data.aws_organizations_organization.current.id
          }
          BoolIfExists = {
            "aws:PrincipalIsAWSService" = "false"
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy_attachment" "identity_perimeter_nonprod" {
  policy_id = aws_organizations_policy.identity_perimeter.id
  target_id = aws_organizations_organizational_unit.nonprod.id
}

Attach to one target per resource block and stage them across separate applies. A single Terraform run that attaches a new RCP to eight OUs at once removes your ability to observe each attachment independently.

Validating the Policy Document in CI

Generic IaC scanners have limited coverage of organization policies, so the checks worth the most here are the ones you write against the documented RCP constraints. Each of these catches a failure mode that otherwise surfaces as an API error at apply time or, worse, as a policy that attaches cleanly and denies nothing.

โœ… A Pre-Apply Gate

#!/usr/bin/env python3
"""Validate an RCP document against documented Organizations constraints."""
import json, sys

SUPPORTED = {  # keep in sync with the AWS Organizations docs
    "s3","sts","kms","sqs","secretsmanager","ecr","aoss","dynamodb","dax",
    "logs","cognito-identity","cognito-idp","events","cloudfront","wafv2",
    # ... full list in the supported-services section above
}
MAX_CHARS = 5120

def validate(path):
    raw = open(path).read()
    doc, errs = json.loads(raw), []

    compact = json.dumps(doc, separators=(",", ":"))
    if len(compact) > MAX_CHARS:
        errs.append(f"size {len(compact)} exceeds {MAX_CHARS} even minified")
    elif len(raw) > MAX_CHARS:
        errs.append(f"size {len(raw)} exceeds limit as written; minify "
                    f"before apply (compacts to {len(compact)})")

    if doc.get("Version") != "2012-10-17":
        errs.append("Version must be 2012-10-17")

    for st in doc.get("Statement", []):
        sid = st.get("Sid", "<no sid>")
        if st.get("Effect") != "Deny":
            errs.append(f"{sid}: Effect must be Deny in a customer RCP")
        if st.get("Principal") != "*":
            errs.append(f"{sid}: Principal must be exactly '*'")
        if "NotPrincipal" in st or "NotAction" in st:
            errs.append(f"{sid}: NotPrincipal/NotAction unsupported in RCPs")

        actions = st.get("Action", [])
        actions = [actions] if isinstance(actions, str) else actions
        if not actions:
            errs.append(f"{sid}: Action is required")
        for a in actions:
            if a == "*":
                errs.append(f"{sid}: bare '*' not permitted in Action")
                continue
            prefix = a.split(":")[0]
            if prefix not in SUPPORTED:
                errs.append(f"{sid}: '{prefix}' is not RCP-supported; "
                            f"this statement will never fire")

        # IfExists discipline: a bare StringNotEquals on a key that may be
        # absent denies requests that never populated it.
        for key in st.get("Condition", {}):
            if key in ("StringNotEquals", "ArnNotLike", "StringNotLike"):
                errs.append(f"{sid}: {key} without IfExists is high risk; "
                            f"absent keys will trigger the deny")
    return errs

if __name__ == "__main__":
    failed = False
    for p in sys.argv[1:]:
        for e in validate(p):
            print(f"{p}: {e}"); failed = True
    sys.exit(1 if failed else 0)

The unsupported-prefix check is the one that pays for itself. A statement denying rds:* is syntactically valid, attaches without complaint, and enforces nothing โ€” the policy looks like a control in your compliance evidence while doing no work. That silent no-op is more dangerous than a policy that fails loudly.

The IfExists check is deliberately a warning rather than an error, because there are legitimate uses of the bare operators. Treat it as something a human confirms in review, not a gate.

๐Ÿงช Testing Behaviour, Not Just Syntax

  • The IAM Policy Simulator does not evaluate RCPs. Do not treat a clean simulation as evidence that a perimeter holds.
  • Test with real cross-account calls from a sandbox. Stand up a caller in an account outside the organization and assert the denial from outside in. Nothing else exercises the actual evaluation path.
  • Assert the exceptions, not only the denials. A perimeter that blocks everything is trivially easy to write. The tests that matter are the ones proving CloudTrail delivery, SSE-KMS, and your named third parties still work.
  • Re-run against the supported-services list on a schedule. The list grows. A service that was out of scope when you wrote the policy may be in scope now, which changes what your wildcards cover.

Diagnosing RCP Denials

An RCP denial looks like every other AccessDenied, which makes attribution the hard part. Work the layers in a fixed order rather than guessing.

๐Ÿ”ฆ Attribution Order

1. Is the resource in a MEMBER account?
   Management account resources are never denied by an RCP.
   If the resource lives there, the RCP is not your culprit.

2. Is the service on the supported list, and does the action
   authorize a resource type?
   Check the "Resource types" column in the Service Authorization
   Reference. An empty column means the RCP never evaluated.

3. Is the caller a service-linked role?
   SLRs are exempt. If an SLR is being denied, look elsewhere.

4. Is the key the condition depends on actually present?
   aws:PrincipalOrgID is absent for federated principals.
   aws:SourceOrgID is absent for several service integrations.
   A bare StringNotEquals on an absent key denies the request.
   This is the single most common self-inflicted RCP outage.

5. Walk the attachment path.
   aws organizations list-policies-for-target \
     --target-id <account-id> \
     --filter RESOURCE_CONTROL_POLICY
   Then repeat for each parent OU up to the root. Any one of
   them can be the source of the deny.

6. Only then look at SCPs, identity policies, resource policies,
   permissions boundaries, session policies, and VPC endpoint
   policies.

Step 4 deserves emphasis because it produces denials that look impossible. A policy that works perfectly for IAM roles fails for every SAML-federated user, because aws:PrincipalOrgID simply is not in the request context for a federated principal โ€” the key is populated only when the calling principal is a member of an organization. The condition evaluates as "not equal," the deny fires, and nothing in the policy looks wrong. The IfExists suffix exists for precisely this case.

๐Ÿ“‹ Enumerating the Full Attachment Path

#!/usr/bin/env bash
# Print every RCP that applies to an account, root-first.
set -euo pipefail
ACCOUNT="$1"
TARGET="$ACCOUNT"
PATH_IDS=()

while :; do
  PATH_IDS=("$TARGET" "${PATH_IDS[@]}")
  PARENT=$(aws organizations list-parents --child-id "$TARGET" \
             --query 'Parents[0].Id' --output text)
  [ "$PARENT" = "None" ] && break
  TARGET="$PARENT"
  [[ "$PARENT" == r-* ]] && { PATH_IDS=("$PARENT" "${PATH_IDS[@]}"); break; }
done

for id in "${PATH_IDS[@]}"; do
  echo "== $id"
  aws organizations list-policies-for-target \
    --target-id "$id" \
    --filter RESOURCE_CONTROL_POLICY \
    --query 'Policies[].{Name:Name,Id:Id}' --output table
done

Because any policy in this path can deny, reading them root-first mirrors the evaluation order and usually surfaces the offending statement faster than starting at the account.

๐Ÿ”‘ Key Takeaways

  • RCPs deny; they never grant. Effective permissions are the intersection of RCPs, SCPs, identity-based policies, and resource-based policies. Removing an identity policy and expecting the RCP to hold access open is a category error.
  • Inheritance is a union of denies. Any RCP in the path from root to account can deny. There is no allow-list to intersect and no way to relax a parent deny from below.
  • The five-service claim is obsolete. The supported list stands at 45 services and includes DynamoDB. Check the current documentation rather than any blog post, including this one.
  • Supported service is not supported action. An RCP only fires on actions that authorize a resource type in the Service Authorization Reference.
  • You get four usable RCPs per entity, at 5,120 characters each. Plan the policy set against that budget before writing the first statement.
  • The management account is outside the perimeter, permanently. Audit it for workload resources before you design anything.
  • Use the IfExists operators. A bare StringNotEquals on a condition key that is absent from the request context evaluates true and fires the deny. This is the most common self-inflicted RCP outage, and it is why aws:PrincipalOrgID breaks federated access.
  • Exempt AWS services with aws:PrincipalIsAWSService, then constrain them with aws:SourceOrgID. The first without the second leaves every AWS service as a confused deputy against your resources.
  • Measure with CloudTrail and Access Analyzer before writing a line of policy. Ninety days minimum. CloudTrail shows what happened; Access Analyzer shows what is currently exposed.
  • Stage attachments sandbox to non-prod to one prod OU to root. Never attach at root first, and remember that a root-level deny cannot be relaxed by anything below it.
  • Gate the policy document in CI. A statement naming a service that RCPs do not support attaches cleanly and enforces nothing โ€” a control that exists only in your compliance evidence.