AWS Organizations

AWS service control policies: practical examples

SCPs set the maximum permissions for member accounts but never grant anything. Prefer deny-list guardrails, exempt global services where needed, and roll out through a test OU.

Published · Updated .

Service control policies (SCPs) are AWS Organizations policies that set the maximum permissions for IAM users and roles in member accounts. They are the right tool for rules that must hold in every account regardless of what that account's administrators do: approved Regions, protected logging, no leaving the organisation.

What SCPs do, and what they do not

  • They never grant permissions. A principal still needs an identity-based or resource-based policy that allows the action. The effective permission is the intersection of what the SCPs allow and what IAM policies allow.
  • They apply to every IAM user and role in member accounts, including the member account's root user.
  • They do not apply to the management account, to service-linked roles, or to principals from accounts outside the organisation (for example, an external account granted access by a bucket policy).
  • Inheritance is restrictive. An account can use an action only if the SCPs at every level above it (root, each OU, the account) allow it. When SCPs are enabled, AWS attaches FullAWSAccess to every node; removing it without a replacement Allow blocks everything.
  • Limits: an SCP can be up to 10,240 characters, and up to 10 SCPs can be attached directly to the root, an OU or an account.

Deny lists vs. allow lists

Most organisations keep FullAWSAccess attached and add SCPs with Deny statements for what must never happen (a deny list). The alternative, replacing FullAWSAccess with SCPs that allow only approved services (an allow list), is stricter but requires updating the SCP whenever a team adopts a new service.

Since September 2025, SCPs support the full IAM policy language, including conditions and specific resource ARNs in Allow statements, NotResource, and NotAction with Allow. That makes allow lists more expressive, but AWS's own guidance still favours explicit Deny statements: an Allow with NotAction also allows every action AWS adds in the future.

Example: restrict member accounts to approved Regions

Region-deny SCP with global-service exemptions · Service control policy (SCP)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "a4b:*",
        "acm:*",
        "aws-marketplace-management:*",
        "aws-marketplace:*",
        "aws-portal:*",
        "billing:*",
        "budgets:*",
        "ce:*",
        "chime:*",
        "cloudfront:*",
        "config:*",
        "consolidatedbilling:*",
        "cur:*",
        "directconnect:*",
        "ec2:DescribeRegions",
        "ec2:DescribeTransitGateways",
        "ec2:DescribeVpnGateways",
        "ecr-public:*",
        "fms:*",
        "freetier:*",
        "globalaccelerator:*",
        "health:*",
        "iam:*",
        "importexport:*",
        "invoicing:*",
        "kms:*",
        "mobileanalytics:*",
        "networkmanager:*",
        "organizations:*",
        "payments:*",
        "pricing:*",
        "route53-recovery-cluster:*",
        "route53-recovery-control-config:*",
        "route53-recovery-readiness:*",
        "route53:*",
        "route53domains:*",
        "s3:GetAccountPublic*",
        "s3:ListAllMyBuckets",
        "s3:ListMultiRegionAccessPoints",
        "s3:PutAccountPublic*",
        "savingsplans:*",
        "shield:*",
        "sts:*",
        "support:*",
        "supportapp:*",
        "supportplans:*",
        "tax:*",
        "trustedadvisor:*",
        "waf-regional:*",
        "waf:*",
        "wafv2:*",
        "wellarchitected:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "eu-central-1",
            "eu-west-1"
          ]
        },
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/BreakGlassAdmin"
        }
      }
    }
  ]
}

The NotAction list exempts global services. IAM, CloudFront, Organizations, Route 53 and Support are served from a single endpoint in us-east-1, as is STS's global endpoint, so requests to them carry aws:RequestedRegion = us-east-1 and would otherwise be denied everywhere outside it. Some services must be used in us-east-1 for global features, such as ACM certificates and WAF web ACLs for CloudFront, so they are exempt too. Treat the list as a starting point. The second condition exempts a break-glass role: a Deny statement applies only when all its conditions match.

An exemption by role name is only as strong as the protection of that role. Without it, any administrator in a member account could create a role with the exempt name, or change the real role's trust policy, and escape the guardrail. The companion SCP denies those changes to the exempt roles for everyone in the account, root user included:

Companion SCP: protect the exempt role and the role that maintains it · Service control policy (SCP)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProtectExemptAndDeployerRoles",
      "Effect": "Deny",
      "Action": [
        "iam:AttachRolePolicy",
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:DeleteRolePermissionsBoundary",
        "iam:DeleteRolePolicy",
        "iam:DetachRolePolicy",
        "iam:PutRolePermissionsBoundary",
        "iam:PutRolePolicy",
        "iam:TagRole",
        "iam:UntagRole",
        "iam:UpdateAssumeRolePolicy",
        "iam:UpdateRole",
        "iam:UpdateRoleDescription"
      ],
      "Resource": [
        "arn:aws:iam::*:role/BreakGlassAdmin",
        "arn:aws:iam::*:role/OrganizationAccountAccessRole"
      ],
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/OrganizationAccountAccessRole"
        }
      }
    },
    {
      "Sid": "OnlySecurityAccountMayAssumeExemptRole",
      "Effect": "Deny",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::*:role/BreakGlassAdmin",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalAccount": "444455556666"
        }
      }
    }
  ]
}

The full example explains how the protected roles are created and maintained, what remains trusted (the management account and anyone who can change SCPs), and how to check the guardrail with dry runs and the policy simulator.

Example: protect audit and detection services

Security baseline SCP · Service control policy (SCP)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProtectCloudTrail",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:DeleteEventDataStore",
        "cloudtrail:DeleteTrail",
        "cloudtrail:PutEventSelectors",
        "cloudtrail:PutInsightSelectors",
        "cloudtrail:StopEventDataStoreIngestion",
        "cloudtrail:StopLogging",
        "cloudtrail:UpdateEventDataStore",
        "cloudtrail:UpdateTrail"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformSecurityAdmin"
        }
      }
    },
    {
      "Sid": "ProtectGuardDuty",
      "Effect": "Deny",
      "Action": [
        "guardduty:Accept*",
        "guardduty:Archive*",
        "guardduty:Create*",
        "guardduty:Delete*",
        "guardduty:Disassociate*",
        "guardduty:Stop*",
        "guardduty:Update*"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformSecurityAdmin"
        }
      }
    },
    {
      "Sid": "ProtectConfig",
      "Effect": "Deny",
      "Action": [
        "config:AssociateResourceTypes",
        "config:DeleteAggregationAuthorization",
        "config:DeleteConfigRule",
        "config:DeleteConfigurationRecorder",
        "config:DeleteDeliveryChannel",
        "config:DeleteEvaluationResults",
        "config:DeleteRetentionConfiguration",
        "config:DeleteServiceLinkedConfigurationRecorder",
        "config:DisassociateResourceTypes",
        "config:PutConfigurationRecorder",
        "config:PutDeliveryChannel",
        "config:PutRetentionConfiguration",
        "config:StopConfigurationRecorder"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformSecurityAdmin"
        }
      }
    },
    {
      "Sid": "ProtectAccountBlockPublicAccess",
      "Effect": "Deny",
      "Action": "s3:PutAccountPublicAccessBlock",
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/PlatformSecurityAdmin"
        }
      }
    },
    {
      "Sid": "DenyLeavingTheOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    }
  ]
}

Attackers who gain administrator access in an account typically try to disable CloudTrail and GuardDuty first. With this SCP, even a member account's root user cannot. organizations:LeaveOrganization is denied because an account that leaves the organisation is no longer subject to any SCP. The same SCP denies s3:PutAccountPublicAccessBlock, so account-level S3 Block Public Access can't be switched off; since November 2025, an AWS Organizations S3 policy can enforce Block Public Access directly, and the SCP then serves as defense in depth. Its exemption needs the same role protection as above; the security baseline example includes it.

Rolling out an SCP safely

  1. Validate it: aws accessanalyzer validate-policy --policy-type SERVICE_CONTROL_POLICY.
  2. Create a test OU, move a sandbox account into it, and attach the SCP there. AWS strongly recommends against attaching untested SCPs to the root.
  3. Use IAM service last accessed data for the OU or accounts to check which services are actually used before denying them.
  4. Exercise allowed and denied calls from the sandbox account (CLI below).
  5. Promote to production OUs one at a time, watching CloudTrail for unexpected denials.
Create, attach to a test OU, verify
aws organizations create-policy --type SERVICE_CONTROL_POLICY --name RegionGuardrail --description 'Approved Regions only' --content file://scp.json
aws organizations attach-policy --policy-id <p-xxxxxxxx> --target-id <ou-test-xxxx>
# from the sandbox account:
aws ec2 describe-vpcs --region us-west-2   # expect: explicit deny in a service control policy

Troubleshooting

  • An access-denied message that mentions a service control policy means an SCP blocked the request. It says nothing about whether IAM policies would have allowed it: explicit denies are evaluated first, so fix the SCP question with the management account (or a delegated policy administrator) and then check the IAM side too.
  • Unexpected denials of console features usually come from a global service missing from a NotAction exemption list.
  • Service-linked roles keep working under SCPs. If a service still acts after a deny, check whether it runs as a service-linked role.

Next

Permissions boundaries vs. SCPs explains when to cap permissions per account and when per role.

Official AWS documentation