← Back to homeaws

SCPs, RCPs and Identity Center: where AWS authorisation actually stands in August 2026

← All writing

We spend a lot of time in other people's AWS Organizations. The pattern is depressingly consistent: a landing zone built in 2022, three SCPs copied from a blog post, root credentials still sitting in twelve member accounts, and an Identity Center directory that nobody has pruned since the last team reshuffle.

Meanwhile the authorisation surface in AWS has changed more in the last eighteen months than in the five years before it. This is our field guide to what's actually worth adopting as of August 2026, what it costs to roll out, and where we've decided the juice isn't worth the squeeze.

The short version

ControlWhat it answersWhere it livesAdopt?
SCP"What may principals in my org do?"Organizations, OU/accountYes — you already have these
RCP"Who may touch my resources, from anywhere?"Organizations, OU/accountYes — highest value/effort ratio available
Declarative policy"What configuration state must accounts hold?"Organizations, OU/accountYes, for EC2/IMDS/public AMI baselines
Centralised root access"Does this account even have root credentials?"Organizations + IAMYes, do it this quarter
Permission boundary"What's the ceiling on a delegated admin's grants?"IAM, per-principalOnly where devs create roles
Identity-aware sessions"Which human did this, three services deep?"Identity CenterYes, where your data tools support it

The mental model that finally made this click for us: SCPs are a filter on the principal side, RCPs are a filter on the resource side, and declarative policies are a filter on the configuration side. They don't grant anything. They only ever subtract.

RCPs: the half of the perimeter nobody built

SCPs have a blind spot that most teams never internalised. An SCP constrains identities in your organisation. It says nothing about an external principal — a partner's role, a forgotten cross-account trust, an anonymous request — touching a bucket that belongs to you. For a decade the only answer was per-resource policy hygiene across thousands of resources.

Resource control policies close that. One policy, attached at the org root, applied to every supported resource in every account underneath.

The canonical version is the data perimeter:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceOrgIdentities",
      "Effect": "Deny",
      "Principal": "*",
      "Action": ["s3:*", "sts:AssumeRole", "kms:*", "sqs:*", "secretsmanager:*"],
      "Resource": "*",
      "Condition": {
        "StringNotEqualsIfExists": { "aws:PrincipalOrgID": "o-abc123xyz" },
        "BoolIfExists": { "aws:PrincipalIsAWSService": "false" }
      }
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "*",
      "Resource": "*",
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}

Two details that bite people:

The PrincipalIsAWSService escape hatch is not optional. CloudTrail writing to your log bucket, Config delivering snapshots, an AWS service role reading a KMS key on your behalf — these arrive with a service principal, not an org member. Omit that condition and you will break logging before you break anything an attacker was doing.

RCPs attach with an implicit RCPFullAWSAccess baseline, the same way SCPs do with FullAWSAccess. Detach it and you deny everything. Don't.

Service coverage started narrow — S3, STS, KMS, SQS, Secrets Manager — and has widened since. Before you write an RCP for a service, check whether it's actually enforced, because an unenforced deny is a false sense of security. We keep a small script in our landing-zone repo that pulls the current supported-service list and diffs it against what our RCPs reference, and it runs weekly in CI.

For genuine partner access, add a second statement carving out named external principals rather than weakening the first one:

{
  "Sid": "AllowNamedPartners",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "*",
  "Condition": {
    "StringNotEqualsIfExists": {
      "aws:PrincipalOrgID": "o-abc123xyz",
      "aws:PrincipalArn": [
        "arn:aws:iam::210987654321:role/partner-etl-reader"
      ]
    },
    "BoolIfExists": { "aws:PrincipalIsAWSService": "false" }
  }
}

The list is a plain resource in Terraform. Adding a partner becomes a pull request with a reviewer, which is the whole point.

Declarative policies: config as a guardrail, not a detection

SCPs and RCPs govern API calls. Declarative policies govern state. You express a desired configuration at the OU level, and accounts underneath can't drift out of it — including accounts created next year by someone who never read your runbook.

The three we set on day one of any engagement:

aws organizations create-policy \
  --type DECLARATIVE_POLICY_EC2 \
  --name baseline-ec2 \
  --content file://ec2-baseline.json
{
  "ec2_attributes": {
    "instance_metadata_defaults": {
      "http_tokens": { "@@assign": "required" },
      "http_put_response_hop_limit": { "@@assign": "2" }
    },
    "image_block_public_access": { "state": { "@@assign": "block_new_sharing" } },
    "snapshot_block_public_access": { "state": { "@@assign": "block_all_sharing" } }
  }
}

IMDSv2-required at the account default level is the single highest-value line in that file. It retires an entire class of SSRF-to-credential-theft, and unlike an SCP on ec2:RunInstances it doesn't need every Terraform module in your estate to pass metadata_options correctly.

You can also set a custom error message so the developer who hits the wall gets a Slack channel instead of a bare UnauthorizedOperation. Do that. Guardrails that don't explain themselves generate tickets for you, not for the developer.

Kill root credentials in member accounts

This is the change with the best effort-to-risk ratio on the whole list, and adoption is still poor.

Enable centralised root access from the management account, then remove root credentials from members entirely. No password, no MFA device to maintain, no access keys, no recovery flow to phish. For the rare legitimate root task — deleting an S3 bucket policy that locked everyone out, unlocking a wedged SQS policy — you take a short-lived, task-scoped privileged session from the management account, and it's in CloudTrail.

aws organizations enable-aws-service-access \
  --service-principal iam.amazonaws.com

aws iam enable-organizations-root-credentials-management
aws iam enable-organizations-root-sessions

# then, per member account
aws iam delete-user-permissions-boundary --help  # no; use:
aws iam delete-root-user-credentials --target-principal 123456789012

We ran this across a 40-account estate in an afternoon. The awkward part is not technical: it's the finance and support contacts still tied to root email addresses, and the two accounts where someone had a root access key from 2019. Audit first with aws iam get-account-summary per account, then delete.

Separately: root MFA enforcement for management accounts and standalone accounts is now simply a fact of life. If you still have an account with no root MFA, you'll discover it at the worst possible moment during a sign-in.

Identity Center: sessions that know who the human is

The long-standing weakness of federated access was attribution decay. A user assumes a permission set, that role calls Athena, Athena reads S3 — and by the time you're looking at the S3 access log, the identity is a role session, not a person. Fine for forensics, useless for authorisation.

Trusted identity propagation carries the Identity Center user identity through the chain, so downstream services can authorise on the user, and access logs record the user. Combined with identity-aware console sessions, you can write Lake Formation and QuickSight grants against groups from your IdP rather than maintaining a parallel role sprawl.

Practical constraints as of now:

  • Coverage is good for the analytics stack (Redshift, Athena, EMR, Lake Formation, QuickSight, S3 Access Grants) and thin elsewhere. Don't plan a migration around services that aren't listed.
  • Your IdP must be the source of truth. If you're still using the Identity Center built-in directory for humans, fix that first — SCIM from Entra ID or Okta, automatic deprovisioning, no exceptions.
  • Group-based assignment only. If you're assigning permission sets to individual users, you'll rebuild this later.

While you're in there: set session duration deliberately. The default of an hour is right for production admin permission sets. Twelve hours for a read-only developer set is a reasonable trade. Eight-hour sessions on an AdministratorAccess permission set are not a trade, they're a decision to not have a control.

Access Analyzer earns its keep now

Two capabilities we now treat as mandatory:

Unused access findings. Point an analyzer at the org, set a tracking period of 60 or 90 days, and get a ranked list of roles, users, permissions and keys nobody has touched. On the average estate we inherit, this identifies 30–50% of IAM roles as dormant. Deleting them is the cheapest attack-surface reduction available.

Internal access findings. The older external-access analyzer told you who outside your org could reach a resource. The internal variant tells you which principals inside your org can reach a critical S3 bucket, DynamoDB table or RDS snapshot — the effective-permissions question you previously had to answer by hand. Scope it to the resources that actually matter; leaving it wide open produces a finding list nobody reads.

And before any policy ships:

aws accessanalyzer validate-policy \
  --policy-type RESOURCE_CONTROL_POLICY \
  --policy-document file://rcp-perimeter.json

Wire that into CI. It catches the malformed condition keys and the overly-broad wildcards that the console will happily accept.

How we roll a policy out without an incident

The failure mode with org-level policies is never the policy being wrong in isolation. It's the policy being right and the estate being non-compliant in a way nobody documented.

Our sequence, unchanged for three years and still correct:

  1. Write the deny as a CloudTrail query first. Before attaching anything, find out how many calls it would have blocked in the last 30 days.
SELECT useridentity.arn, eventsource, eventname, count(*) AS n
FROM cloudtrail_logs
WHERE eventtime > date_add('day', -30, now())
  AND eventsource = 's3.amazonaws.com'
  AND useridentity.type != 'AWSService'
  AND COALESCE(useridentity.sessioncontext.sessionissuer.arn, '') NOT LIKE '%o-abc123xyz%'
GROUP BY 1,2,3
ORDER BY n DESC
LIMIT 100;
  1. Attach to a sandbox OU of one account. Live for a week.
  2. Attach to non-production OUs. Live for two weeks. Watch the support channel, not the dashboard.
  3. Production, during a low-traffic window, with the detach command already in a terminal.
  4. Terraform the whole thing so step 4 is never done by hand again.

And keep the quotas in view — they're the reason "just add another statement" eventually stops working:

LimitValue
SCPs per OU or account5
RCPs per OU or account5
Policy document size5,120 characters
OU nesting depth5
Effective policy = intersection down the treealways

Five kilobytes goes faster than you think once you're listing partner ARNs. Whitespace-strip your JSON in the pipeline; it buys you 15–20% for free.

Where we don't bother

We don't write SCPs that duplicate a declarative policy — the declarative version enforces state, the SCP only blocks one API path to that state. We don't use permission boundaries in teams where nobody creates IAM roles; it's a control with real cognitive cost and no benefit there. And we're sceptical of the enormous "deny 200 dangerous actions" SCPs that circulate: they consume your character budget, they're impossible to reason about at 3am, and most of what they block is already blocked by not granting it.

The boring version wins. A tight RCP perimeter, IMDSv2 by declaration, no root credentials, group-driven Identity Center, and Access Analyzer deleting the roles nobody uses. That's a weekend of work and it removes most of what we find on assessments.

If you'd like a second pair of eyes on an Organizations layout before you attach anything to the root — that's the kind of review we do most weeks.

Enjoyed the read? We build this stuff for clients too.

Start a project