Service control policies
An SCP defines the maximum permissions available to accounts in an organisational unit. It does not grant anything; it removes the ability to do things that no account in that OU should ever do.
How SCPs work
- They apply to member accounts, not the management account.
- They are evaluated before identity policies. An action allowed by IAM is still denied if an SCP denies it.
- An account must have at least one
Allow. The defaultFullAWSAccesspolicy provides it; custom policies that list allowed services replace it where a stricter posture is wanted. - Deny statements are the primary tool. Allow lists are used where the approved service set is genuinely narrow.
Baseline policies
These are applied to every workload OU unless a documented exception exists.
Protect the audit trail
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDisableCloudTrail",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail"
],
"Resource": "*"
},
{
"Sid": "DenyDisableConfig",
"Effect": "Deny",
"Action": [
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"config:DeleteDeliveryChannel"
],
"Resource": "*"
},
{
"Sid": "DenyDisableGuardDuty",
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:UpdateDetector",
"guardduty:DisassociateFromMasterAccount"
],
"Resource": "*"
}
]
}
Deny root user
{
"Version": "2012-10-17",
"Statement": {
"Sid": "DenyRootUser",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {"aws:PrincipalArn": "arn:aws:iam::*:root"}
}
}
}
The exception path for root is a break-glass procedure with an approved change and an alarm.
Require IMDSv2
{
"Version": "2012-10-17",
"Statement": {
"Sid": "RequireImdsV2",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"NumericLessThan": {"ec2:RoleDelivery": "2.0"}}
}
}
IMDSv1 allows a server-side request forgery to retrieve instance credentials. Requiring v2 removes that path.
Deny long-lived credentials
{
"Version": "2012-10-17",
"Statement": {
"Sid": "DenyIamUserAndKeyCreation",
"Effect": "Deny",
"Action": ["iam:CreateAccessKey", "iam:CreateUser"],
"Resource": "*"
}
}
Region enforcement
{
"Version": "2012-10-17",
"Statement": {
"Sid": "RestrictRegion",
"Effect": "Deny",
"NotAction": [
"a4b:*", "budgets:*", "ce:*", "cloudfront:*", "cur:*", "health:*",
"iam:*", "organizations:*", "route53:*", "route53domains:*",
"shield:*", "support:*", "trustedadvisor:*", "waf:*", "wellarchitected:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {"aws:RequestedRegion": ["us-east-1", "ap-south-1"]}
}
}
}
Global services are excluded through NotAction. The approved region list is set
per customer.
Block public access
{
"Version": "2012-10-17",
"Statement": {
"Sid": "DenyPublicS3",
"Effect": "Deny",
"Action": [
"s3:PutBucketPublicAccessBlock",
"s3:PutBucketAcl",
"s3:PutAccountPublicAccessBlock"
],
"Resource": "*",
"Condition": {
"StringEquals": {"s3:x-amz-acl": ["public-read", "public-read-write"]}
}
}
}
Paired with account-level S3 Block Public Access, which is the stronger control.
Protect network boundaries
Deny changes that would make a private VPC internet-accessible, and deny deletion of VPC Flow Logs:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyFlowLogDeletion",
"Effect": "Deny",
"Action": [
"ec2:DeleteFlowLogs",
"logs:DeleteLogGroup",
"logs:DeleteLogStream"
],
"Resource": "*"
},
{
"Sid": "DenyIgwAttach",
"Effect": "Deny",
"Action": ["ec2:AttachInternetGateway"],
"Resource": "*"
}
]
}
Require tags
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRunInstanceWithoutProjectTag",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": ["arn:aws:ec2:*:*:instance/*", "arn:aws:ec2:*:*:volume/*"],
"Condition": {"Null": {"aws:RequestTag/Project": "true"}}
}
]
}
Extend across the resource types that carry cost: RDS, ECS, Lambda, S3, DynamoDB, Secrets Manager.
Deny leaving the organisation
{
"Version": "2012-10-17",
"Statement": {
"Sid": "DenyLeaveOrganization",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
}
}
Protect critical roles
{
"Version": "2012-10-17",
"Statement": {
"Sid": "DenyCriticalRoleChanges",
"Effect": "Deny",
"Action": [
"iam:AttachRolePolicy", "iam:DeleteRole", "iam:DeleteRolePolicy",
"iam:DetachRolePolicy", "iam:PutRolePolicy", "iam:UpdateAssumeRolePolicy"
],
"Resource": "arn:aws:iam::*:role/OrganizationAccountAccessRole",
"Condition": {
"StringNotLike": {"aws:PrincipalARN": "arn:aws:iam::*:role/platform-admin"}
}
}
}
Quarantine
For a suspected-compromised account, apply:
{
"Version": "2012-10-17",
"Statement": {"Sid": "Quarantine", "Effect": "Deny", "Action": "*", "Resource": "*"}
}
This stops all activity in the account while preserving the evidence and the ability for the security account to investigate.
Policy set by OU
| Policy | Sandbox | Dev | Stage | Prod | Security |
|---|---|---|---|---|---|
| Deny root user | ✓ | ✓ | ✓ | ✓ | ✓ |
| Protect audit trail | ✓ | ✓ | ✓ | ✓ | ✓ |
| Deny leaving organisation | ✓ | ✓ | ✓ | ✓ | ✓ |
| Require IMDSv2 | ✓ | ✓ | ✓ | ✓ | |
| Deny long-lived credentials | ✓ | ✓ | ✓ | ✓ | |
| Region restriction | ✓ | ✓ | ✓ | ||
| Block public access | ✓ | ✓ | ✓ | ✓ | |
| Require tags | ✓ | ✓ | ✓ | ||
| Protect network boundaries | ✓ | ✓ | ✓ | ||
| Quarantine (on demand) | ✓ | ✓ | ✓ | ✓ | ✓ |
Operating rules
- Test in sandbox first. SCPs fail closed and can break a running workload.
- Version and review. Policies live in a repository and are reviewed like code.
- Document every exception. An exception is time-bound and reviewed at expiry.
- Monitor denials. CloudTrail records
AccessDeniedevents caused by SCPs; review them for signals of attempted misuse as well as for accidental friction. - Never attach an SCP that removes all allows. An OU with no allow statement cannot be used, including by the people who need to fix it.