Skip to content

SCP vs RCP vs Declarative Policies in AWS

SCP vs RCP vs declarative policy: a decision rule for AWS Organizations, generic policy examples, 2026 quotas and a rollout method that avoids outages.

13 min read

Once an AWS organization has more than a handful of accounts, guardrails stop being something each account team sets up for itself. A typical request: S3 Block Public Access switched on in every account, and kept on. AWS Organizations has three policy types that look like they could do that, a service control policy (SCP), a resource control policy (RCP) and a declarative-style policy. Only one of them can actually hold the setting, and picking the wrong one either leaves a gap or blocks your own fix.

The obvious control is an SCP denying s3:PutBucketPublicAccessBlock. Check CloudTrail before you write it: in a real estate most calls to that API are Terraform turning protection on. No condition key separates enabling Block Public Access from disabling it, so the deny stops the safe change along with the dangerous one.

The three types answer different questions. SCPs limit what your own principals can do, RCPs limit how anyone can reach your resources, and declarative policies hold a setting at a fixed value, so the useful question for any guardrail is whether you need to deny a verb or assert a state. The examples come from governing a real multi-account estate, written up as a multi-account AWS governance case study. Every policy is generic, with placeholders for Regions and AWS's documentation example account 123456789012 for every account ID.

SCP vs RCP vs declarative policy at a glance#

All three are authorization or management policies in AWS Organizations, attached to the root, an OU (organizational unit, a folder of accounts) or an account. Strictly, AWS calls the EC2 type a declarative policy (DECLARATIVE_POLICY_EC2) and the S3 one an Amazon S3 policy (S3_POLICY). Both use the same syntax and hold a setting, so this guide groups them as "declarative".

SCP RCP Declarative (EC2) / S3 policy
What it limits IAM users and roles in member accounts, including the member root user Requests to your resources, from any principal A service setting, held at a fixed value
Grants access? Never Never Not an access policy; it sets state
Management account Not affected Not affected Not stated in AWS docs; test before relying on it
Service-linked roles Not affected Not affected Governed
Principals outside your org Not affected Affected Not applicable
Report-only mode None None Some attributes have one (Allowed AMIs audit_mode, VPC Encryption Controls attempt_monitor)
DescribeEffectivePolicy Not supported Not supported Supported (EC2 and S3 types)
Size and per-target limit 10,240 characters, 10 5,120 characters, 5 10,000 characters, 10

Sources: the AWS Organizations user guide pages for SCPs, RCPs and declarative policies.

The service-linked roles row matters most: AWS services acting on your behalf slip past SCPs and RCPs, but not past a declarative policy, which the service enforces in its control plane. And none of the three grants anything. If someone proposes an SCP "to give the team access", send the plan back before it starts.

Deny a verb or assert a state#

I run every proposed guardrail through three questions:

  1. Who is acting? If the control is about your own users and roles doing something, it is an SCP.
  2. What resource is being reached? If the control is about any caller touching your data (including outsiders), it is an RCP.
  3. What setting must hold? If the control is "this must be on", look for a declarative attribute first.
flowchart LR A[New guardrail] --> B{Is it a setting that must hold a value?} B -- Yes --> C{Declarative attribute exists?} C -- Yes --> D[Declarative policy] C -- No --> E{Can a condition key isolate the unsafe direction?} B -- No --> F{Is it about who can reach a resource?} F -- Yes --> G[RCP] F -- No --> H E -- Yes --> H[SCP] E -- No --> I[Set the value first, then deny changes to it]

Start from the end state you want, not the API you want to block.

Block Public Access is the clean example. The Organizations S3 policy asserts "on" for every account in scope, and in my test Terraform's bucket-level calls kept succeeding (more on that in the declarative section). It also avoids an ordering trap. If you deny the account-level Block Public Access API before the value is set, you've blocked your own fix until you detach the SCP or add an exemption, while with the S3 policy Organizations sets the value for you.

When a team asks for a guardrail, ask them to describe the state they want rather than the action they want to block. The answer usually picks the tool.

SCPs for your own principals#

An SCP is a ceiling on IAM permissions in member accounts, and that includes member accounts acting as delegated administrators. An action must be allowed at every level from the root down to the account, and a deny at any level wins. That's why the default FullAWSAccess SCP must stay attached or be replaced by your own allow list.

A baseline that has held up well for me covers five things: no leaving the organization, no closing accounts, no turning off the main security services, no new unencrypted EBS volumes, and no root user minting access keys, IAM users or passwords. Treat the action lists below as a starting point: each service has more off switches (for example guardduty:UpdateDetector and cloudtrail:UpdateTrail).

baseline-scp.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeaveOrgAndCloseAccount",
      "Effect": "Deny",
      "Action": ["organizations:LeaveOrganization", "account:CloseAccount"],
      "Resource": "*"
    },
    {
      "Sid": "ProtectSecurityServices",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "guardduty:DeleteDetector",
        "securityhub:DisableSecurityHub",
        "access-analyzer:DeleteAnalyzer"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/AWSControlTowerExecution"
        },
        "StringNotEquals": {
          "aws:PrincipalAccount": "123456789012"
        }
      }
    },
    {
      "Sid": "DenyUnencryptedEbs",
      "Effect": "Deny",
      "Action": ["ec2:CreateVolume", "ec2:RunInstances"],
      "Resource": "arn:aws:ec2:*:*:volume/*",
      "Condition": {
        "Bool": { "ec2:Encrypted": "false" }
      }
    },
    {
      "Sid": "ProtectEbsDefaultEncryption",
      "Effect": "Deny",
      "Action": "ec2:DisableEbsEncryptionByDefault",
      "Resource": "*"
    },
    {
      "Sid": "DenyRootCredentials",
      "Effect": "Deny",
      "Action": ["iam:CreateAccessKey", "iam:CreateUser", "iam:CreateLoginProfile"],
      "Resource": "*",
      "Condition": {
        "ArnLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" }
      }
    }
  ]
}

The two conditions in the security statement are ANDed, so the deny skips both the Control Tower role and the security tooling account (123456789012 here). It exempts the whole account because that account is the delegated administrator for these services and manages them for every member. If you can, narrow it to the specific roles there. EBS encryption-by-default has no declarative attribute, so protecting it stays an SCP job.

Turn on EBS encryption-by-default first

Enable EBS encryption-by-default in every account and Region before you attach the unencrypted-volume deny. Otherwise every volume create that does not ask for encryption fails.

I keep the Region restriction in a separate SCP. Region rules and data-protection rules change at different speeds, and a separate policy keeps the blast radius of each edit small. The pattern follows AWS's deny-by-requested-Region example. The NotAction list here is a short sample of global services; extend it with every global service you use before attaching.

region-scp.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "sts:*",
        "support:*",
        "route53:*",
        "cloudfront:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["${APPROVED_REGION_1}", "${APPROVED_REGION_2}"]
        },
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/AWSControlTowerExecution"
        }
      }
    }
  ]
}

Sweep every service before a Region lock

A pre-check that only lists EC2 resources misses S3 buckets in non-approved Regions. S3 bucket operations resolve to the bucket's Region, so once the SCP lands, such a bucket can no longer be managed. Inventory every service in every Region first.

If you use Control Tower, know that registering an OU attaches only its own aws-guardrails-* SCP. Your baseline needs its own attachment, and you shouldn't edit or detach Control Tower's SCPs. If you're moving accounts between organizations, the order of operations is in how to move an AWS account to another organization.

I handle exceptions with placement, not conditions: put the account that needs one in an OU without the policy, or attach the policy to its siblings individually. That's easier to audit than a tangle of exemption conditions. When you plan the work, budget for a baseline SCP, a separate Region SCP and a pre-attach inventory across every service. The inventory is the part teams skip.

RCPs for the data perimeter#

An RCP is evaluated on the resource side and, unlike an SCP, applies to principals outside your organization too. That makes it the right place for data-perimeter rules: "nobody reaches these resources without TLS", whoever they are.

Beyond the table's exceptions, RCPs also skip AWS-managed KMS keys and kms:RetireGrant. They cover a growing list of services, including S3, STS, KMS, SQS, Secrets Manager, DynamoDB, ECR, Cognito, CloudWatch Logs and OpenSearch Serverless. RDS is not on it, so an RCP will not protect an RDS or Aurora database. The TLS-only policy here is AWS's published example, with a Sid added.

tls-only-rcp.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceSecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:*",
        "sts:*",
        "kms:*",
        "sqs:*",
        "secretsmanager:*",
        "cognito-identity:*",
        "cognito-idp:*",
        "logs:*",
        "dynamodb:*",
        "ecr:*",
        "aoss:*"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": { "aws:SecureTransport": "false" }
      }
    }
  ]
}

Pilot it on a non-production OU first. S3 website endpoints serve HTTP only, so a CloudFront distribution whose origin is an S3 website endpoint talks to it over HTTP, and under a TLS-only RCP that origin breaks. Find those distributions before you attach.

An RCP is how you protect data from callers you don't control. Before anyone assumes it protects a given data store, ask which services it covers.

AWS declarative policies vs SCP#

A declarative policy does not block an API. It tells the service what the setting is, and the service enforces it in its control plane. Three properties follow:

  • It governs service-linked roles, which SCPs cannot.
  • It stays in force as the service adds new APIs, so there is no deny list to keep current.
  • New accounts inherit it the moment they join the OU.
s3-policy.json
{
  "s3_attributes": {
    "public_access_block_configuration": {
      "@@assign": "all"
    }
  }
}
ec2-declarative.json
{
  "ec2_attributes": {
    "image_block_public_access": {
      "state": { "@@assign": "block_new_sharing" }
    },
    "snapshot_block_public_access": {
      "state": { "@@assign": "block_new_sharing" }
    },
    "serial_console_access": {
      "status": { "@@assign": "disabled" }
    }
  }
}

The S3 policy makes public buckets private on attach

Setting all turns on all four Block Public Access settings and overrides the account's own. A bucket serving public content today stops serving it the moment the policy applies. Find those buckets first, then attach the policy to the other accounts individually, or move the account that needs public access to an OU without it.

The S3 policy has a single attribute, public_access_block_configuration, and it is all or none. AWS's guidance is to disable the S3 policy type if you want to manage Block Public Access per account. It shipped in November 2025, so guides written before then don't mention it. On detach, the setting rolls back to its pre-attach value, so know that value before you count on detach as a rollback.

In my test, with the S3 organization policy in force, a bucket-level PutBucketPublicAccessBlock call still succeeded. Public bucket policies and public ACLs were denied, because S3 applies the most restrictive combination of settings. That was a single test, so verify it yourself before you depend on it.

The EC2 declarative attributes also cover VPC Block Public Access, Allowed AMIs, IMDS defaults and VPC Encryption Controls. An EC2 declarative policy can replace SCP statements that block AMI and snapshot sharing, but prove the new coverage is a strict superset of the old before you remove anything.

IMDSv2 enforcement can stop launches

AWS warns that turning on http_tokens_enforced while launches still allow IMDSv1 (http_tokens optional, from a launch template, launch call or AMI default) causes launch failures. Set http_tokens to required and find IMDSv1 callers first. I left that attribute out deliberately.

For snapshots I chose block_new_sharing over block_all_sharing. block_new_sharing leaves anything already public as it is and stops new public shares, so it can't cut off a consumer you haven't found yet. block_all_sharing makes existing public snapshots private the moment it applies, so inventory them first. Neither mode affects sharing with specific accounts.

For Block Public Access and AMI or snapshot sharing, I'd choose an S3 or EC2 declarative policy over an SCP. It covers more principals and needs no maintenance as APIs change.

Quotas changed in 2026#

AWS raised the SCP quotas on 15 May 2026. Current values from the Organizations quotas page, which does not list the S3 policy type separately:

Policy type Max size Max attached per root, OU or account
SCP 10,240 characters 10
RCP 5,120 characters 5 (RCPFullAWSAccess uses one)
Declarative (EC2) 10,000 characters 10

Inherited policies do not count toward a target's limit. A policy type must be enabled on the root before you can use it. Enabling SCPs auto-attaches FullAWSAccess; enabling the S3 policy or EC2 declarative policy type attaches nothing. RCP slots are the scarce ones, so plan RCPs as a few broad policies rather than one per rule.

Rolling out without breaking production#

Testing is where these tools are weakest. There is no report-only mode for SCPs or RCPs. The IAM policy simulator evaluates SCPs that already apply but does not support RCPs, and it will not simulate a draft you have not attached. AWS's advice is to test on a test OU or account, use CloudTrail and last-accessed data, then move up the tree. Checking coverage afterwards is awkward too: DescribeEffectivePolicy does not support SCPs or RCPs, so to confirm an account is covered, walk its parent chain with ListParents and call ListPoliciesForTarget at each level.

validate-before-create.sh
aws accessanalyzer validate-policy \
  --policy-type SERVICE_CONTROL_POLICY \
  --policy-document file://baseline-scp.json

IAM Access Analyzer's validate-policy catches invented action names. For example, there is no s3:DeleteBucketPublicAccessBlock; deleting maps to the Put permission. It also recommends ArnLike over StringLike on ARN condition keys, which is why validation is the first line of the checklist I run for every attachment.

Comparing deployed policy JSON

AWS returns policy documents with keys and arrays in varying order. A plain string diff reports drift that is not there. Sort keys and arrays recursively on both sides before comparing.

  • Validate the JSON with Access Analyzer before creating the policy
  • Sweep every service in every Region for resources the policy would strand
  • Set any value you are about to protect (encryption-by-default, Block Public Access) first
  • Find every bucket that is public today before attaching the S3 policy
  • For EC2 declarative policies, generate the Organizations account status report first
  • Attach to a canary OU with non-production accounts
  • Prove a fast detach works, and know what the setting reverts to
  • Walk each account's parent chain to confirm coverage
  • Check CloudTrail for new access denied errors the next day
  • Move up one OU at a time

When one of these lands in a change request, look for the canary OU, the detach plan and the next-day CloudTrail check. If any of them is missing, the rollout isn't ready.

What to do next#

List every guardrail you have and the state each one is meant to guarantee. Anything that reads "this setting must be on" moves to a declarative policy, anything about outsiders reaching your data belongs in an RCP, and the rest is SCP work.

Then run the checklist above on one policy and one low-risk OU. If you want a second pair of eyes on an estate before you start, book a free intro call; the multi-account governance case study shows the kind of estate this came from.

Frequently asked questions

What is the difference between an SCP and an RCP?

An SCP caps what IAM users and roles in your member accounts can do, whichever resource they call. An RCP caps how anyone, including principals outside your organization, can reach your resources in supported services. Neither grants access. Use an SCP to limit your own people and workloads, and an RCP to guard the resources themselves.

AWS declarative policies vs SCP: when should I use a declarative policy?

Use a declarative policy (EC2) or an S3 policy when you want a setting held at a fixed value, such as S3 Block Public Access or EC2 image sharing. It is enforced in the service's control plane, covers service-linked roles and keeps working as services add APIs. Use an SCP when you need to deny an action and a condition key can describe the dangerous case.

Can I test an SCP or RCP before attaching it?

Only partly. There is no report-only mode for either. The IAM policy simulator evaluates SCPs that already apply but does not support RCPs. AWS advises attaching to a test OU or account first, watching CloudTrail and last-accessed data, then moving up the tree. Plan a canary OU and a fast detach.

Do SCPs apply to the management account?

No. SCPs do not affect the management account, service-linked roles or principals outside your organization. They do apply to member accounts that are delegated administrators, and to the root user of every member account. Keep workloads out of the management account, because no SCP will stop anything there.

What are the SCP quotas in 2026?

Since AWS raised them in May 2026, an SCP can be up to 10,240 characters and you can attach up to 10 to a root, OU or account. RCPs stay at 5,120 characters and 5 per target, and the default RCPFullAWSAccess uses one of those slots. Inherited policies do not count toward the per-target limit.

Found this useful? Share it

AWSDevOpsSecurity

Written by Shahid Yousuf

Senior Software Engineer building secure, scalable web applications, AI solutions and cloud infrastructure for businesses worldwide. Work with me or follow along via RSS.

Shahid Yousuf

Senior Software Engineer

I build and consult on software across web, mobile, cloud and AI. Have something in mind? Let's talk.

Hire me Book a free intro call See case studies

Get new posts

No newsletter, no tracking. Follow along in any feed reader.

GitHub Actions OIDC Trust Policy for AWS

GitHub now issues two OIDC subject formats. Here is a trust policy that accepts both, a way to prove it against every repository, and a safe order for retiring keys.

AWS15 min read

Move an AWS Account to Another Organization

The order of operations for moving an AWS account between organizations: invite from the destination, enroll in Control Tower, then close the sign-on, guardrail and old-role gaps.

AWS12 min read

Have a project in mind?

Share your goals, timeline and any constraints, whether you need it built or want expert advice. I read every message myself, as the engineer who would build it, and reply with a clear view on approach, scope and next steps.

Hire me