SCPs and RCPs: Using Both to Close the Preventive Control Gap
Service Control Policies restrict what your principals can do. Resource Control Policies restrict what can be done to your resources. Most AWS organisations use one or neither. Here is why both matter, and how to choose.
Detection is the domain Ira operates in. But preventive controls decide how much there is to detect in the first place. In AWS Organizations, the two most powerful preventive controls are Service Control Policies (SCPs) and Resource Control Policies (RCPs). They are often treated as interchangeable. They are not.
AWS introduced RCPs in late 2024 as a distinct policy type, not a replacement for SCPs. The distinction matters because they answer different questions. SCPs answer: what can my principals do? RCPs answer: what can be done to my resources? Both questions need an answer, and neither policy type answers both.
The asymmetry SCPs leave open
An SCP that denies s3:PutObject unless the request includes server-side encryption is a common baseline. It stops any principal inside the organisation from uploading unencrypted objects to any bucket in the organisation. That is the intent, and it works — for traffic originating inside the org.
What the SCP does not stop is an external principal — a role in a partner’s AWS account, a federated identity from a vendor, a compromised IAM user belonging to someone outside your organisation — from writing an unencrypted object to one of your buckets, if your bucket policy allows them access. The SCP attaches to the principal’s account. External principals are outside that attachment scope.
This is the gap RCPs are designed to close. An RCP attached to the bucket’s account applies to every request that touches the bucket, regardless of where the principal lives. The same encryption rule expressed as an RCP would deny the unencrypted PutObject call from the external principal, because the policy evaluates at the resource boundary, not the principal boundary.
Where RCPs can and cannot reach
RCPs support five services as of writing: S3, STS, KMS, SQS, and Secrets Manager. These are the services where resource-based policies already exist — bucket policies, key policies, queue policies, trust policies on roles. RCPs layer an organisation-wide deny over the top, ensuring that however permissive a resource-based policy is set by an individual account, the organisation’s baseline holds.
The services RCPs do not cover — EC2, Lambda, DynamoDB, RDS, every other AWS service — remain in SCP territory. SCPs apply across all AWS services, because they operate on the principal’s identity and IAM evaluates identity-based permissions for every API call.
This produces a clean division for policy design:
- Rules about who your principals are allowed to be, and what they are allowed to call → SCP
- Rules about what must be true of any request that touches your S3, STS, KMS, SQS, or Secrets Manager resources → RCP
For a service like EC2, only SCPs apply. For S3, both apply, and the choice is about which side of the principal/resource boundary you want to enforce from.
The syntactic differences that trip teams up
Three policy-element differences matter when writing or porting policies:
- RCPs require
"Principal": "*". SCPs do not support thePrincipalelement at all — it is implicit from the account the SCP is attached to. When migrating an SCP to an RCP,Principal: "*"is the field you add. - RCPs do not support
NotAction. If your SCP usesNotActionto express deny everything except this set of read-only calls, that pattern does not translate directly to an RCP — you have to enumerate the denied actions explicitly. - Both support
NotResource, but only SCPs supportNotAction. The asymmetry matters when porting: a compact SCP written withNotActioncan blow up into a much longer RCP once every denied action is listed out.
These constraints interact with the 5,120-character policy size limit. A policy that fits as an SCP using NotAction may not fit as an RCP written with explicit Action lists, and vice versa. This is not a theoretical concern — teams with mature SCP practices routinely run against the size ceiling, which is part of why RCPs were introduced. You now have two separate budgets.
The attachment budgets
The organisation-wide ceilings:
- 10,000 SCPs total per organisation
- 1,000 RCPs total per organisation
- 5 policies of each type attached to any single entity (root, OU, account)
The per-entity limit is the operational one. An account can have five SCPs and five RCPs attached to it — ten policies evaluated on every call to a covered resource. One SCP slot is consumed by the default FullAWSAccess policy, and one RCP slot is consumed by the automatic RCPFullAWSAccess policy once RCPs are enabled, so the usable budget is four of each. That is enough headroom for a layered model where the organisation root carries the baseline, OUs carry environment-specific constraints, and individual accounts carry exceptions, but it requires discipline. Un-consolidated policies accumulate quickly.
A layered deny example
Consider the requirement: no principal, internal or external, may read from the production data bucket from outside the approved VPC endpoints.
Expressed purely as an SCP, the policy denies s3:GetObject on the bucket unless the request comes through one of the listed VPC endpoints. This stops internal principals from exfiltrating through public S3 endpoints. It does not stop a partner’s role from reading the bucket directly over the internet if the bucket policy grants them access — the partner is outside the SCP scope.
Expressed as an RCP attached to the bucket’s account, the same rule applies to every request, internal or external. An external partner attempting to read over a non-approved path is denied regardless of what the bucket policy says.
The right answer is usually both. The SCP stops internal principals from developing habits that the RCP would later block anyway (fail fast, fail locally). The RCP stops external principals the SCP cannot reach. Defense in depth is not a slogan here — it is a statement about which policy type evaluates in which situation.
Migration is not a rewrite
Teams sometimes treat the arrival of RCPs as an opportunity to collapse their SCP set into RCPs. This is usually wrong. The boundary the policy is enforcing at genuinely differs:
- An SCP expresses we do not trust our own principals to follow this rule.
- An RCP expresses we do not trust the resource-based policy on this resource to be correct in isolation.
These are different trust statements. An encryption requirement that is purely about internal-principal behaviour should stay in an SCP — moving it to an RCP changes the scope in a way that may not match the original threat model. Meanwhile, a rule designed to prevent cross-account data exposure belongs in an RCP, because the SCP version could never cover the external-principal case in the first place.
The practical migration pattern is additive: identify rules currently in SCPs whose intent was always protect this resource from anyone, and add an RCP equivalent. Keep the SCP. The two policies evaluate independently; both have to allow the request for it to proceed.
How this connects to detection
SCPs and RCPs are preventive. They stop calls at evaluation time. Ira is detective — it sees what happened and reconstructs the chain. The connection is in what each tells you about the other.
When Ira surfaces a privilege escalation chain that succeeded, the after-action question is not only who issued the calls? but which preventive control would have blocked this sequence? A well-designed SCP on iam:CreatePolicy with a condition on principal tag could have stopped step one. A cross-account policy evaluation failure would have shown up in CloudTrail as an explicit deny — a much clearer signal than the successful calls that comprised the real attack.
Every detection Ira produces is also a map of where the preventive controls did not reach. The escalation chain that ran in 57 seconds, surfaced as a high-severity incident, is a concrete specification for the SCP or RCP that would have made the chain structurally impossible. Detection and prevention feed each other; neither is sufficient alone.
Downloadable templates
Two starter policies, designed to be dropped into an AWS Organization root and then tightened to your environment:
- Baseline SCP — denies leaving the organisation, disabling CloudTrail/GuardDuty/Config/Security Hub, deleting VPC flow logs, root-user actions, key destruction on
Security=criticaltagged KMS keys, access-key creation for human identities, CloudTrail log-bucket tampering, and activity in unapproved regions. Review theNotActionregion allow-list and the approved-region list before applying. - Baseline RCP — denies S3, KMS, Secrets Manager, SQS, and STS access from any principal outside your organisation, enforces TLS for S3, and applies confused-deputy protection via
aws:SourceOrgIDon role assumption. Replaceo-REPLACE-MEwith your organisation ID and adjust theTrustedVendorRoleARN list to match your partner integrations.
Both templates are under the 5,120-character policy size limit, leaving headroom for environment-specific additions. Test in a non-production OU first — an SCP or RCP deny is evaluated on every API call, and a misconfigured condition can break legitimate workflows within seconds of attachment.
The short version
- SCPs control principals. RCPs control resources. Both are needed.
- RCPs cover five services. Everything else is SCP-only territory.
- Mind the element differences: RCPs require
Principal: "*"and do not supportNotAction; SCPs do not supportPrincipalat all. - Budget accordingly: five of each per entity (four usable after the default full-access policies), separate organisation-wide ceilings.
- Migrate additively. The original SCP usually still earns its slot.
- Use detection output to find the preventive gaps. The attack chains Ira surfaces are the policy specifications you did not know you were missing.