CoursesAWS security engineeringSCP guardrails & landing zones

SCP guardrails & landing zones

Preventive limits and secure-by-default accounts.

Advanced30 min · lesson 13 of 15

A compromised admin's first move is usually to kill the alarms. Delete the CloudTrail, the running log of who did what, so nobody can replay what happens next. Switch off GuardDuty. Then run up a five-figure bill mining crypto in some region nobody on the team ever looks at. If your entire defense rests on every person with admin access choosing, every single day, not to do any of that, you don't really have a defense. You have a hope. So this lesson answers a blunt question: how do you write a rule that your own account admins, and even the all-powerful root user, simply cannot break?

Rules even the admin can't break

An apartment building can wire certain limits into its structure. The lift won't stop on a floor without the right keycard. The upper windows only open a few inches, by design. A tenant can paint the walls and rearrange the furniture all they like, but the wiring behind those limits sits somewhere they simply can't reach, and so does the building manager. A service control policy, or SCP, is that kind of limit for an AWS account. It's a ceiling on what anyone in the account is permitted to do, and it binds everyone underneath it: every user and role in IAM (short for Identity and Access Management, the AWS system that decides who's allowed to do what), the account's own administrator, and even the root user who normally answers to no one.

There are two ways to enforce any rule. You can watch for someone breaking it and raise an alarm after the fact, or you can stop the action from happening in the first place. Watching matters, but it's always a step behind: the damage is done, and now you're writing an incident report. An SCP works from the other direction. Almost everything you do in AWS, from creating a storage bucket to shutting down a server, travels as an API call, a request your tools fire off on your behalf. The SCP sits in the path of every one of those calls, and if the policy forbids the call, AWS refuses it before anything happens. One detail trips people up constantly, so get it straight now. An SCP never grants a single permission. It only subtracts. A principal, meaning any user or role making a request, still needs an IAM policy that allows the action; the SCP just draws the outer boundary that no grant, however generous, can reach past. The action has to be allowed on the inside and not denied on the outside, and any deny, anywhere, wins outright.

guardrails-scp.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RegionLock",
"Effect": "Deny",
"NotAction": [
"iam:*", "sts:*", "organizations:*", "cloudfront:*",
"route53:*", "waf:*", "support:*", "cur:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["eu-west-1", "eu-central-1"]
}
}
},
{
"Sid": "DenyLeaveOrg",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
},
{
"Sid": "ProtectSecurityServices",
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromAdministratorAccount",
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder"
],
"Resource": "*"
}
]
}

Read that first statement slowly, because its shape is deliberately odd. It denies everything (NotAction is the mirror image of Action, so it means 'every action except these') whenever the request targets a region that isn't eu-west-1 or eu-central-1. That aws:RequestedRegion check is a condition AWS attaches to nearly every call. The NotAction list is the escape hatch, and it's load-bearing: a handful of AWS services are global and quietly answer from us-east-1, so denying them by region would break sign-in, DNS (the internet's address book that turns a name like example.com into a machine address), and the organization API itself. The second statement stops any account from detaching itself from the organization, which would otherwise drop it clean out of reach of every other guardrail. The third guards the security tooling: nobody stops CloudTrail, deletes a trail, disables the Config recorder, or kills the GuardDuty detector. Those are precisely the switches an intruder flips first to go dark.

Create it, then attach it to an OU

An AWS organization is a tree. The root sits at the top; under it hang organizational units (OUs) that group accounts by purpose, say a Security OU, a Workloads OU, and a Sandbox OU; and inside each OU sit the actual accounts. Attach an SCP to an OU and it flows down to every account beneath it, the way a notice posted for an entire floor binds every flat on that floor. Where you attach it decides the blast radius, and this is the single most important operational choice you'll make. Pin a strict region lock to the root and you've just governed every account you own, including the one quietly running a content delivery network (a CDN, the setup that serves your site's images and video fast to users worldwide) that you'd half forgotten about. You'll find out at 2am. Attach it to the Workloads OU instead and only those accounts feel it. Start narrow, watch for what breaks, then widen deliberately.

create the SCP and attach it to the Workloads OU
$ aws organizations create-policy \
--name guardrails-baseline \
--description "Region lock, no-leave-org, protect security services" \
--type SERVICE_CONTROL_POLICY \
--content file://guardrails-scp.json
{
"Policy": {
"PolicySummary": {
"Id": "p-a1b2c3d4",
"Arn": "arn:aws:organizations::111122223333:policy/o-8xk2mp1q7a/service_control_policy/p-a1b2c3d4",
"Name": "guardrails-baseline",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": false
}
}
}
$ aws organizations attach-policy \
--policy-id p-a1b2c3d4 \
--target-id ou-8xk2-w0rkl0ad
$ echo $?
0
$ aws organizations list-targets-for-policy --policy-id p-a1b2c3d4 \
--query 'Targets[].{Name:Name,Type:Type}'
[
{
"Name": "Workloads",
"Type": "ORGANIZATIONAL_UNIT"
}
]

So the fence is up. The only honest way to trust it is to try climbing over. Sign in to a workload account sitting under that OU, as a full administrator, and attempt the exact things the policy forbids.

the same actions, now refused for an admin
$ aws s3api create-bucket --bucket acme-scratch-99 \
--region us-west-2 \
--create-bucket-configuration LocationConstraint=us-west-2
An error occurred (AccessDenied) when calling the CreateBucket operation:
User: arn:aws:sts::444455556666:assumed-role/AppAdmin/alex is not authorized to
perform: s3:CreateBucket on resource: "arn:aws:s3:::acme-scratch-99"
with an explicit deny in a service control policy
$ aws guardduty delete-detector \
--detector-id 1cb2f3e4d5a6b7c8d9e0f1a2b3c4d5e6 --region eu-west-1
An error occurred (AccessDeniedException) when calling the DeleteDetector operation:
User: arn:aws:sts::444455556666:assumed-role/AppAdmin/alex is not authorized to
perform: guardduty:DeleteDetector with an explicit deny in a service control policy
$ aws organizations leave-organization
An error occurred (AccessDeniedException) when calling the LeaveOrganization
operation: You don't have permission to access the specified resource,
with an explicit deny in a service control policy

Look at who got refused. The role alex assumed carries AdministratorAccess, an IAM policy that allows every action in the account. It didn't help at all. The SCP's explicit deny sits above that grant and overrides it, which is the whole point. This is the line an IAM policy alone can never hold, because an admin can always rewrite an IAM policy to give themselves back whatever it took away. An admin cannot reach up and edit the SCP, because SCPs are managed one level up, inside the organization, from a management account this workload admin is locked out of.

A region lock bricks global services if you forget the escape hatch
The most common way to take down your own accounts with an SCP is a region lock that denies everything outside the approved regions and forgets to exempt the global services. IAM, STS, CloudFront, Route 53, WAF, and Organizations all answer from us-east-1 under the hood, so a blanket region deny with no NotAction list blocks sign-in, role assumption, and DNS across every account it touches, and you can lock people out with no way back in short of AWS Support. Always exempt those services in a NotAction escape hatch, and test on a throwaway Sandbox OU before you go near production. One more trap worth burning into memory: SCPs never apply to the management account. Never run workloads there, and never rely on an SCP to protect it.

Secure on day zero

Writing one good SCP is the easy part. The hard part is guaranteeing that every account, including the one someone spins up in a hurry on a Friday afternoon to unblock a demo, arrives with the guardrails already switched on. That's the job of a landing zone. AWS Control Tower, and its more customizable cousin the Landing Zone Accelerator, gives you an account factory. A team requests an account, and the factory hands one back that's already dropped into the correct OU, already covered by the guardrail SCPs, already wired into central sign-in, already streaming its logs to a locked archive account, with GuardDuty and Config turned on from the first second. There's no window where the account sits exposed, and no runbook step anyone can quietly skip. Hand-built accounts drift the moment there's more than one of them; a factory produces the same fenced account every time.

Control Tower shows the baseline controls on the OU
$ aws controltower list-enabled-controls \
--target-identifier arn:aws:organizations::111122223333:ou/o-8xk2mp1q7a/ou-8xk2-w0rkl0ad
{
"enabledControls": [
{
"controlIdentifier": "arn:aws:controltower:eu-west-1::control/AWS-GR_REGION_DENY",
"arn": "arn:aws:controltower:eu-west-1:111122223333:enabledcontrol/AbC123XyZ",
"targetIdentifier": "arn:aws:organizations::111122223333:ou/o-8xk2mp1q7a/ou-8xk2-w0rkl0ad",
"statusSummary": { "status": "SUCCEEDED" }
},
{
"controlIdentifier": "arn:aws:controltower:eu-west-1::control/AWS-GR_RESTRICT_ROOT_USER",
"arn": "arn:aws:controltower:eu-west-1:111122223333:enabledcontrol/DeF456UvW",
"targetIdentifier": "arn:aws:organizations::111122223333:ou/o-8xk2mp1q7a/ou-8xk2-w0rkl0ad",
"statusSummary": { "status": "SUCCEEDED" }
}
]
}

Those AWS-GR_ controls are Control Tower's own managed guardrails, the same idea as the SCP you wrote by hand, just packaged and version-managed by AWS. You've probably also noticed one piece of the baseline keeps surfacing: the org trail delivering every account's activity to a locked archive account. That trail is the evidence everything else depends on, and it's worth building so carefully that a compromised admin can't quietly edit history. That's exactly where the next lesson goes.

How a landing zone fences every account
Organization hierarchy
Management account
org + billing admin; SCPs never apply here
Security OU
log-archive + audit accounts
Workloads OU
prod & dev accounts, tightly fenced
Guardrail SCPs (inherited by every account below)
RegionLock
deny all actions outside approved regions
DenyLeaveOrg
a member cannot detach itself
ProtectSecurityServices
no stopping GuardDuty / CloudTrail / Config
Control Tower account factory
vends a new account
placed straight into the right OU
baseline pre-applied
SCPs, org trail, Identity Center, GuardDuty on
secure on day zero
no unprotected window, nothing to remember
SCPs are ceilings that flow downhill. Grants added below can raise the floor but never lift the ceiling, and the management account sits outside the fence entirely.
Quick check
01You attach a region-lock SCP to the Workloads OU that denies every action whose aws:RequestedRegion isn't eu-west-1, with no NotAction exceptions at all. What happens to the accounts under that OU?
Incorrect — Backwards. SCPs apply to every account under the OU and are the one thing that never touches the management account.
Incorrect — AWS auto-exempts nothing. Without a NotAction escape hatch, global calls get denied along with everything else.
Correct — Global endpoints live in us-east-1, so a blanket region deny with no NotAction blocks them and can lock people out.
Incorrect — NotAction is perfectly valid. The policy attaches cleanly and then causes the outage, which is what makes this trap dangerous.
02Which statement correctly describes how a service control policy (SCP) interacts with IAM (Identity and Access Management) permissions?
Correct — the action must be allowed on the inside by IAM and not denied on the outside by the SCP, and a deny anywhere overrides an allow.
Incorrect — SCPs only subtract and never grant; principals still need an IAM policy that allows the action.
Incorrect — Listing an action in an SCP does not allow it; an SCP only sets the ceiling and never confers access.
Incorrect — Both gates are evaluated on every call, so an IAM allow cannot get past an SCP deny.
03Your guardrail SCP is attached to the organization root and denies cloudtrail:StopLogging. An attacker who has compromised credentials in the management account runs StopLogging on the organization trail, and it succeeds. Why?
Incorrect — StopLogging is not in the global-service exemption; it is explicitly named in the security-services deny, so that is not what let it through.
Incorrect — SCPs apply regardless of when an account was created, so account age is not the factor here.
Correct — the management account sits outside the SCP fence entirely, which is exactly why you never run workloads or rely on SCP protection there.
Incorrect — An explicit SCP deny beats any IAM allow, including AdministratorAccess, so that is not what allowed the call.

Try this

Work through “Secure on day zero” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a region lock bricks global services if you forget the escape hatch. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related