← Back to blog
Cloud / AWSMay 17, 2026· 11 min

A Practical SCP Guardrail Library for AWS Organizations

A field-tested set of Service Control Policies that cap what any account can do, with exception patterns that survive break-glass and CI/CD reality. Copy, scope, deploy.

A Practical SCP Guardrail Library for AWS Organizations

Service Control Policies are the only control in AWS that can stop a principal an attacker has fully compromised, including one wielding AdministratorAccess. IAM policies grant; SCPs cap. If a credential is exfiltrated and the actor tries to disable CloudTrail or pivot into an unused region, a well-placed SCP turns that from a breach into a denied API call in your event log. This article is a working guardrail library: each section is one policy you can lift, scope to your Organizational Units, and ship. The guardrails are organized as a small number of broad deny policies rather than dozens of narrow ones, because the per-account SCP size budget is finite and you will hit it faster than you expect.

Before you deploy anything: SCPs never grant permissions, they only set the maximum available. An action is allowed only if both an IAM policy AND every SCP in the account's path allow it. SCPs do NOT apply to the management (payer) account, so never rely on them to constrain the org root, and never run workloads there. Prefer explicit Deny with NotAction over Allow-list SCPs, because Allow SCPs silently shrink the permission ceiling for every service you forgot to list. Test in a sandbox OU first, and always carve out a break-glass role in your Deny conditions before you attach to production.

Guardrail 1: Protect the security telemetry plane

The first move in most cloud intrusions after credential theft is to blind the defenders. Deny the API calls that stop, delete, or tamper with CloudTrail, GuardDuty, AWS Config, and Security Hub across the whole org. The exception is a dedicated security-automation role identified by aws:PrincipalArn, so your detective-control pipeline can still manage these services. Keep that role's ARN identical in every account (same path and name) so one condition block covers the fleet.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyTamperWithSecurityTelemetry",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "cloudtrail:UpdateTrail",
        "cloudtrail:PutEventSelectors",
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromMasterAccount",
        "guardduty:UpdateDetector",
        "guardduty:StopMonitoringMembers",
        "config:DeleteConfigurationRecorder",
        "config:StopConfigurationRecorder",
        "config:DeleteDeliveryChannel",
        "securityhub:DisableSecurityHub",
        "securityhub:DeleteMembers"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/SecurityAutomationRole"
        }
      }
    }
  ]
}

Guardrail 2: Neutralize the root user

Member-account root credentials should never be used for daily work, yet they retain powers no IAM policy can revoke. Since 2024 AWS Organizations supports centralized root access management, but an SCP is still your belt-and-suspenders. The aws:PrincipalIsAWSService key lets legitimate service-linked calls through, while the root principal itself is blocked from doing anything. Note that this does not affect the management account's root, which SCPs never reach.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUserActions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" },
        "BoolIfExists": { "aws:PrincipalIsAWSService": "false" }
      }
    }
  ]
}

Guardrail 3: Region lock

Region lock shrinks your attack surface and your bill at once: an attacker cannot spin up crypto-mining fleets in af-south-1 if no principal can call EC2 there. Use aws:RequestedRegion in a Deny, and exempt the genuinely global services (IAM, Organizations, CloudFront, Route 53, WAF for CloudFront, Support, Artifact) with NotAction, since their endpoints resolve to us-east-1 regardless of where the caller sits. Add aws:PrincipalArn carve-outs for any platform role that legitimately operates cross-region.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RegionLock",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "cloudfront:*",
        "route53:*",
        "route53domains:*",
        "waf:*",
        "wafv2:*",
        "support:*",
        "artifact:*",
        "sts:*",
        "globalaccelerator:*",
        "shield:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [ "ap-south-1", "eu-west-1" ]
        },
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformGlobalRole"
        }
      }
    }
  ]
}

Guardrail 4: Prevent accounts from leaving the org

Every SCP in this library is enforced only while the account is a member of your organization. The moment a compromised admin calls LeaveOrganization, all of these guardrails evaporate and the account becomes a standalone billing entity under the attacker's control. Deny the escape hatch outright, and pair it with a deny on disabling the org-managed integrations a member account could use to detach itself from delegated administration.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization",
        "organizations:DeleteOrganization",
        "organizations:RemoveAccountFromOrganization"
      ],
      "Resource": "*"
    }
  ]
}

Guardrail 5: Keep S3 buckets private

Public S3 buckets remain a leading cause of data exposure. Rather than chase individual ACL and bucket-policy permutations, lock the account-level and bucket-level Block Public Access settings so no principal can weaken them, and deny the ACL and policy calls that would open a bucket. Scope the exception to your storage-platform role via aws:PrincipalArn, and add aws:PrincipalOrgID so the condition only relaxes for principals that are genuinely part of your organization, never an externally assumed role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyWeakeningS3PublicAccessBlock",
      "Effect": "Deny",
      "Action": [
        "s3:PutBucketPublicAccessBlock",
        "s3:PutAccountPublicAccessBlock",
        "s3:PutBucketAcl",
        "s3:PutBucketPolicy",
        "s3:PutObjectAcl"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/StoragePlatformRole"
        },
        "StringEquals": {
          "aws:PrincipalOrgID": "o-exampleorgid"
        }
      }
    }
  ]
}

A note on the logic above: the Deny fires when the principal is NOT the storage role AND the call is made by a member of your org. If you want to also block any cross-account principal from these calls, drop the aws:PrincipalOrgID condition entirely so the Deny applies regardless of org membership, and rely solely on the ArnNotLike exception.

Guardrail 6: Require IMDSv2 on every instance

The 2019 Capital One breach hinged on a server-side request forgery that read instance credentials from IMDSv1. IMDSv2's session-token requirement defeats that class of attack. Deny RunInstances and ModifyInstanceMetadataOptions whenever the request does not set the metadata mode to required, using the ec2:MetadataHttpTokens and ec2:MetadataHttpEndpoint condition keys. This forces the secure default at the API boundary instead of hoping every launch template gets it right.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRunInstancesWithoutIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    },
    {
      "Sid": "DenyWeakeningInstanceMetadata",
      "Effect": "Deny",
      "Action": "ec2:ModifyInstanceMetadataOptions",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpEndpoint": "disabled"
        },
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformGlobalRole"
        }
      }
    }
  ]
}

Deployment and operating notes

  • Attach broad deny policies high in the OU tree (for example a Sandbox vs Production split), and reserve account-level attachment for genuine exceptions. The effective ceiling is the intersection of every policy on the path.
  • Mind the quotas: a maximum of 5 SCPs attach directly to any single entity, and each policy document is capped at 5,120 characters after whitespace is removed. Consolidate related denies into one statement array rather than spreading them thin.
  • Keep break-glass role ARNs out of every Deny condition and protect those roles with their own monitoring, because they are the keys that bypass the entire library.
  • Stand up the org-managed CloudTrail and a delegated-admin security account first; an SCP that protects CloudTrail is worthless if the trail it protects does not exist or logs to a deletable bucket in the same account.
  • Roll out in audit mode by attaching to a single test OU, replaying real CI/CD and human workflows, and watching for AccessDenied events before you widen the blast radius.
  • Version-control every policy and deploy through your IaC pipeline so changes are reviewed; a fat-fingered Allow SCP can lock an entire OU out of a service in seconds.

None of these guardrails replaces least-privilege IAM, but together they form a floor that holds even when an identity is fully compromised. Start with telemetry protection and the leave-organization deny, since those two preserve your ability to detect and to keep enforcing everything else, then layer in region lock, root denial, S3 hardening, and IMDSv2 as you validate each against your real workloads.

Learn it by doing

Spin up a real AWS security lab, or explore our training tracks.

24 people viewing now