DEV Community

Mario
Mario

Posted on • Originally published at blog.leastprivilege.cloud

A Starting Point for AWS Service Control Policies

A team opens an AWS account. They deploy everything in us-east-1. Reasonable choice.

Six months later, their AWS bill has EC2 instances running in ap-southeast-1.

Nobody on the team worked in that region. Nobody authorized those instances. They weren't in any Terraform state. They didn't appear in any monitoring dashboard because GuardDuty wasn't enabled there.

A credential had been compromised three weeks earlier. The attacker scanned the account, saw that monitoring was concentrated in us-east-1, and picked a region where nobody was watching. They ran there quietly for weeks.

That's the case that made me take SCPs seriously.

Not a compliance requirement. Not a Well-Architected checklist item. A bill with instances nobody recognized, in a region nobody was watching.


What SCPs Actually Do

AWS Organizations lets you attach Service Control Policies to an organizational unit. An SCP sets a permission ceiling. It defines the maximum permissions any account in that OU can use, regardless of what IAM says.

If your IAM policy allows an action and your SCP denies it, the action is denied. SCP wins.

That's the leverage. One policy, attached at the OU level, governs every account underneath it, including accounts that don't exist yet.


A Practical OU Structure

Before you write a single SCP, you need a structure to attach it to.

The simplest model that works in practice:

Root
├── Security       (audit account, log archive)
├── Infrastructure (shared services, networking)
├── Workloads
│   ├── Production
│   ├── Staging
│   └── Development
└── Sandbox
Enter fullscreen mode Exit fullscreen mode

Each OU gets its own SCPs. Production gets the strictest. Sandbox gets the most relaxed. Development sits in the middle, with enough freedom to build but without the blast radius of production.

In practice, many organizations end up with a different shape. One OU per project, or one per business unit. Both work. The structure matters less than the rule: accounts that share the same security requirements belong in the same OU.

One mistake I see repeatedly is applying everything at Root. Root-level SCPs apply to every member account, including your security account, your audit account, your billing tools. One important exception: SCPs never apply to the management account, regardless of what you attach at Root. Apply at Root only what must be true everywhere without exception. Everything else belongs at the OU level.


SCP Examples Worth Considering

Quick note before the code: these are examples, not a definitive list. Your environment will be different. You may need fewer of these, more of them, or completely different ones. Take what's useful and leave the rest.

One more thing about the format. Each block below is a single SCP statement, not a complete policy. To use one, drop it into the Statement array of a policy document:

{
  "Version": "2012-10-17",
  "Statement": [ /* one or more of the statements below */ ]
}
Enter fullscreen mode Exit fullscreen mode

You can combine several statements into one SCP, as long as the whole policy stays under the 5,120-character limit.

Example: Deny Non-Approved Regions

AWS accounts have multiple regions enabled by default. Most teams use two or three. The rest are attack surface with no monitoring.

When a credential is compromised, attackers look for regions where GuardDuty isn't enabled and where no one is watching CloudTrail. This SCP closes that gap by denying API calls to regions outside your approved list.

{
  "Sid": "DenyNonApprovedRegions",
  "Effect": "Deny",
  "NotAction": [
    "iam:*",
    "sts:*",
    "route53:*",
    "cloudfront:*",
    "budgets:*",
    "ce:*",
    "support:*",
    "organizations:*",
    "account:*",
    "waf:*",
    "globalaccelerator:*",
    "health:*"
  ],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": {
      "aws:RequestedRegion": [
        "us-east-1",
        "us-west-2"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use NotAction, not Action: *. Services like IAM, STS, Route53, and CloudFront are global, not regional. If you use Action: *, you break them. This is the most common mistake when teams write this SCP for the first time.

The NotAction list above is simplified for readability. The official AWS reference list is much longer and includes services like config:*, kms:*, sso:*, and wafv2:*. Before applying this in production, compare against the AWS Control Tower region deny policy to avoid blocking legitimate cross-region operations.

Adjust the approved region list to match where your workloads actually run.

Apply this to a sandbox OU first. As written, it can also block your IaC deployment role and AWS service-linked operations across regions, so watch CloudTrail for AccessDenied before promoting it to production.

Example: Deny Large Instance Types

This looks like a cost control. It's also a security control.

When an attacker compromises credentials, they launch GPU instances for cryptomining. The financial damage from a credential leak drops significantly when the largest instance they can run is an m5.xlarge. This SCP limits the impact of a compromised credential, not just the bill.

{
  "Sid": "DenyLargeInstances",
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "StringNotLike": {
      "ec2:InstanceType": [
        "t3.*",
        "t2.*",
        "m5.large",
        "m5.xlarge",
        "m5.2xlarge",
        "c5.large",
        "c5.xlarge"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Adjust the allowed list to match your actual workload needs. If your production workloads require larger instances, add them explicitly. The goal is to make the approved list specific enough that anything outside it stands out.

Example: Deny Disabling Security Services

This is the one that stops attackers from going dark after they get in.

Standard post-compromise playbook: gain access, disable GuardDuty, delete CloudTrail, operate without detection. This SCP breaks that sequence. Even with AdministratorAccess in IAM, the attacker cannot disable these services. SCP overrides IAM.

{
  "Sid": "DenyDisableSecurityServices",
  "Effect": "Deny",
  "Action": [
    "guardduty:DeleteDetector",
    "guardduty:DisassociateFromAdministratorAccount",
    "guardduty:StopMonitoringMembers",
    "guardduty:UpdateDetector",
    "securityhub:DeleteHub",
    "securityhub:DisableSecurityHub",
    "securityhub:DisassociateFromAdministratorAccount",
    "cloudtrail:DeleteTrail",
    "cloudtrail:StopLogging",
    "cloudtrail:UpdateTrail",
    "config:DeleteConfigurationRecorder",
    "config:StopConfigurationRecorder",
    "config:DeleteConfigRule",
    "access-analyzer:DeleteAnalyzer"
  ],
  "Resource": "*"
}
Enter fullscreen mode Exit fullscreen mode

If you use SNS for security alerting, add cloudwatch:DisableAlarmActions and sns:DeleteTopic. An attacker who can't disable the detector can still silence the alarm.

One caveat on guardduty:UpdateDetector. It blocks every update to the detector, not just the ones that weaken it, so changing finding-publishing frequency or enabling a new feature goes through the same deny. If that friction bothers your team, scope it with a condition instead of denying the action outright.

Example: Deny Disabling Logging

Logs are forensic evidence. An attacker who can't turn off detection services will try to erase the trail instead.

{
  "Sid": "DenyDisableLogging",
  "Effect": "Deny",
  "Action": [
    "cloudtrail:DeleteTrail",
    "cloudtrail:StopLogging",
    "logs:DeleteLogGroup",
    "logs:DeleteLogStream",
    "logs:DeleteRetentionPolicy",
    "ec2:DeleteFlowLogs",
    "wafv2:DeleteLoggingConfiguration"
  ],
  "Resource": "*"
}
Enter fullscreen mode Exit fullscreen mode

You'll notice cloudtrail:DeleteTrail and cloudtrail:StopLogging appear here and in the previous example. That overlap is fine. If you apply both statements, the duplication costs nothing. If you'd rather keep one source of truth, drop them from whichever statement you apply second.

One operational note: logs:DeleteLogGroup will block developers from cleaning up log groups in test environments. Apply this SCP to production and staging OUs only. For development, use a CloudWatch Logs retention policy instead of blocking deletion entirely.

For RDS and ELB logging, SCPs are a poor fit. The actions that control their logging configuration are too broad and block legitimate operations. A more reliable approach is AWS Config rules with automatic remediation: if logging gets disabled, Config detects it and re-enables it. Same result, less friction.


What Control Tower Actually Covers

If you're on AWS Control Tower, it's worth reading the actual SCP content before assuming you're protected.

CloudTrail: Control Tower's mandatory SCP for CloudTrail targets this resource:

"Resource": ["arn:aws:cloudtrail:*:*:trail/aws-controltower-*"]
Enter fullscreen mode Exit fullscreen mode

It only protects trails named aws-controltower-*. Any trail you create yourself is not covered. And if you're on Landing Zone 4.0 or above, this SCP was removed entirely. AWS deprecated it when Control Tower moved away from account-level trails.

AWS Config: This one is different. The mandatory Config SCP uses "Resource": ["*"], so it does protect your Config setup broadly, not just Control Tower's.

GuardDuty and Security Hub: No mandatory preventive guardrail by default. There are elective options, but you have to enable them explicitly per OU.

The practical check: open your Organizations console, read the SCP attached to your OU, and look at the Resource field. If it's scoped to aws-controltower-* resources, it's not protecting yours.


When SCPs Are Not Enough

SCPs are preventive controls. They stop things from happening.

They don't tell you what happened before you put them in place. They don't detect misconfigurations that already exist. They don't replace monitoring.

The full picture: SCPs for prevention, AWS Config for drift detection, GuardDuty and Security Hub for threat detection, CloudTrail with a centralized destination for forensics.

SCPs are the foundation. Not the ceiling.

If you're implementing these for the first time, start with the region restriction and the security services protection. Those two give you the most immediate security return. Instance types and logging have more operational nuance and will need adjustment for your workloads.

Start there. Tune as you go.

Prevention is the first layer. The next one is seeing what's happening inside the accounts you just put a ceiling on. In the next article I'll build a lightweight SIEM on AWS without buying anything, using the logs these guardrails keep an attacker from deleting.


Tags: AWS, Cloud Security, IAM, Cloud Governance, DevSecOps

Top comments (0)