Skip to main content

AWS Organizations SCP Region Restriction

Overview

This check verifies that your AWS Organization has a Service Control Policy (SCP) in place to restrict operations to only approved AWS regions. Without this control, users and services could deploy resources in any region worldwide, potentially bypassing your security monitoring and compliance requirements.

Risk

If this check fails, your organization faces several risks:

  • Data residency violations: Resources could be created in regions that violate regulatory requirements (GDPR, HIPAA, etc.)
  • Security blind spots: Attackers could use ungoverned regions to hide malicious activity from your monitoring tools
  • Cost surprises: Uncontrolled regional sprawl leads to unexpected charges and difficult-to-track resources
  • Incident response gaps: Security teams may not monitor all regions, creating detection and response delays

Remediation Steps

Prerequisites

  • Access to the AWS Organizations management account (not a member account)
  • Permissions to create and attach Service Control Policies
  • SCPs must be enabled in your organization

AWS Console Method

  1. Sign in to the AWS Organizations console using your management account

  2. In the left navigation, choose Policies, then Service control policies

  3. If SCPs are not enabled, choose Enable service control policies

  4. Choose Create policy

  5. Enter a name (e.g., DenyUnapprovedRegions) and description

  6. Replace the default policy content with the following (adjust the region list to match your approved regions):

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllOutsideAllowedRegions",
"Effect": "Deny",
"NotAction": [
"a4b:*",
"access-analyzer:*",
"account:*",
"acm:*",
"aws-marketplace:*",
"aws-portal:*",
"billing:*",
"budgets:*",
"ce:*",
"cloudfront:*",
"config:*",
"cur:*",
"globalaccelerator:*",
"health:*",
"iam:*",
"importexport:*",
"invoicing:*",
"organizations:*",
"pricing:*",
"route53:*",
"route53domains:*",
"shield:*",
"sso:*",
"sts:*",
"support:*",
"trustedadvisor:*",
"waf:*",
"wafv2:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"us-east-1",
"us-west-2"
]
}
}
}
]
}
  1. Choose Create policy

  2. Select your new policy, then choose Actions > Attach policy

  3. Select the root, organizational units (OUs), or accounts where you want to enforce region restrictions

  4. Choose Attach policy

AWS CLI (optional)

Step 1: Create the policy JSON file

Save the following to a file named region-restriction-policy.json:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllOutsideAllowedRegions",
"Effect": "Deny",
"NotAction": [
"a4b:*",
"access-analyzer:*",
"account:*",
"acm:*",
"aws-marketplace:*",
"aws-portal:*",
"billing:*",
"budgets:*",
"ce:*",
"cloudfront:*",
"config:*",
"cur:*",
"globalaccelerator:*",
"health:*",
"iam:*",
"importexport:*",
"invoicing:*",
"organizations:*",
"pricing:*",
"route53:*",
"route53domains:*",
"shield:*",
"sso:*",
"sts:*",
"support:*",
"trustedadvisor:*",
"waf:*",
"wafv2:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"us-east-1",
"us-west-2"
]
}
}
}
]
}

Step 2: Create the SCP

aws organizations create-policy \
--name "DenyUnapprovedRegions" \
--description "Denies access to AWS services outside of approved regions" \
--type SERVICE_CONTROL_POLICY \
--content file://region-restriction-policy.json \
--region us-east-1

Save the PolicyId from the output (e.g., p-abcd1234).

Step 3: Attach the policy to a target

Replace <policy-id> with your policy ID and <target-id> with your root ID (e.g., r-abcd), OU ID (e.g., ou-abcd-12345678), or account ID:

aws organizations attach-policy \
--policy-id <policy-id> \
--target-id <target-id> \
--region us-east-1

Step 4: Verify the attachment

aws organizations list-policies-for-target \
--target-id <target-id> \
--filter SERVICE_CONTROL_POLICY \
--region us-east-1
CloudFormation (optional)

Deploy this template from the management account of your AWS Organization.

AWSTemplateFormatVersion: '2010-09-09'
Description: Service Control Policy to restrict AWS operations to approved regions

Parameters:
AllowedRegions:
Type: CommaDelimitedList
Description: List of AWS regions where operations are permitted
Default: us-east-1,us-west-2

PolicyName:
Type: String
Description: Name for the Service Control Policy
Default: DenyUnapprovedRegions

TargetId:
Type: String
Description: The ID of the root, OU, or account to attach the policy to
AllowedPattern: ^(r-[a-z0-9]{4,32}|ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|\d{12})$
ConstraintDescription: Must be a valid root ID (r-), OU ID (ou-), or account ID (12 digits)

Resources:
RegionRestrictionSCP:
Type: AWS::Organizations::Policy
Properties:
Name: !Ref PolicyName
Description: Denies access to AWS services outside of approved regions
Type: SERVICE_CONTROL_POLICY
TargetIds:
- !Ref TargetId
Content:
Version: '2012-10-17'
Statement:
- Sid: DenyAllOutsideAllowedRegions
Effect: Deny
NotAction:
- a4b:*
- access-analyzer:*
- account:*
- acm:*
- artifact:*
- aws-marketplace:*
- aws-portal:*
- billing:*
- budgets:*
- ce:*
- chatbot:*
- chime:*
- cloudfront:*
- cloudsearch:*
- cloudwatch:PutMetricData
- config:*
- cur:*
- datapipeline:GetAccountLimits
- directconnect:*
- ec2:DescribeRegions
- ec2:DescribeTransitGateways
- ec2:DescribeVpnGateways
- ecr-public:*
- fms:*
- freetier:*
- globalaccelerator:*
- health:*
- iam:*
- importexport:*
- invoicing:*
- kms:*
- mobileanalytics:*
- networkmanager:*
- organizations:*
- payments:*
- pricing:*
- resource-explorer-2:*
- route53:*
- route53-recovery-cluster:*
- route53-recovery-control-config:*
- route53-recovery-readiness:*
- route53domains:*
- s3:CreateMultiRegionAccessPoint
- s3:DeleteMultiRegionAccessPoint
- s3:DescribeMultiRegionAccessPointOperation
- s3:GetAccountPublicAccessBlock
- s3:GetBucketLocation
- s3:GetMultiRegionAccessPoint
- s3:GetMultiRegionAccessPointPolicy
- s3:GetMultiRegionAccessPointPolicyStatus
- s3:GetStorageLensConfiguration
- s3:GetStorageLensDashboard
- s3:ListAllMyBuckets
- s3:ListMultiRegionAccessPoints
- s3:ListStorageLensConfigurations
- s3:PutAccountPublicAccessBlock
- s3:PutMultiRegionAccessPointPolicy
- savingsplans:*
- shield:*
- sso:*
- sts:*
- support:*
- supportapp:*
- supportplans:*
- sustainability:*
- tag:GetResources
- tax:*
- trustedadvisor:*
- waf:*
- waf-regional:*
- wafv2:*
Resource: '*'
Condition:
StringNotEquals:
aws:RequestedRegion: !Ref AllowedRegions

Outputs:
PolicyId:
Description: The ID of the created Service Control Policy
Value: !Ref RegionRestrictionSCP
Export:
Name: !Sub ${AWS::StackName}-PolicyId

Deploy the stack:

aws cloudformation create-stack \
--stack-name region-restriction-scp \
--template-body file://template.yaml \
--parameters \
ParameterKey=AllowedRegions,ParameterValue="us-east-1,us-west-2" \
ParameterKey=TargetId,ParameterValue=r-abcd \
--region us-east-1
Terraform (optional)

This configuration creates an SCP and attaches it to specified targets.

terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}

variable "allowed_regions" {
description = "List of AWS regions where operations are permitted"
type = list(string)
default = ["us-east-1", "us-west-2"]
}

variable "policy_name" {
description = "Name for the Service Control Policy"
type = string
default = "DenyUnapprovedRegions"
}

variable "target_ids" {
description = "List of root, OU, or account IDs to attach the policy to"
type = list(string)
default = []
}

locals {
# Global services that should be excluded from region restrictions
global_services = [
"a4b:*",
"access-analyzer:*",
"account:*",
"acm:*",
"artifact:*",
"aws-marketplace:*",
"aws-portal:*",
"billing:*",
"budgets:*",
"ce:*",
"chatbot:*",
"chime:*",
"cloudfront:*",
"cloudsearch:*",
"cloudwatch:PutMetricData",
"config:*",
"cur:*",
"datapipeline:GetAccountLimits",
"directconnect:*",
"ec2:DescribeRegions",
"ec2:DescribeTransitGateways",
"ec2:DescribeVpnGateways",
"ecr-public:*",
"fms:*",
"freetier:*",
"globalaccelerator:*",
"health:*",
"iam:*",
"importexport:*",
"invoicing:*",
"kms:*",
"mobileanalytics:*",
"networkmanager:*",
"organizations:*",
"payments:*",
"pricing:*",
"resource-explorer-2:*",
"route53:*",
"route53-recovery-cluster:*",
"route53-recovery-control-config:*",
"route53-recovery-readiness:*",
"route53domains:*",
"s3:CreateMultiRegionAccessPoint",
"s3:DeleteMultiRegionAccessPoint",
"s3:DescribeMultiRegionAccessPointOperation",
"s3:GetAccountPublicAccessBlock",
"s3:GetBucketLocation",
"s3:GetMultiRegionAccessPoint",
"s3:GetMultiRegionAccessPointPolicy",
"s3:GetMultiRegionAccessPointPolicyStatus",
"s3:GetStorageLensConfiguration",
"s3:GetStorageLensDashboard",
"s3:ListAllMyBuckets",
"s3:ListMultiRegionAccessPoints",
"s3:ListStorageLensConfigurations",
"s3:PutAccountPublicAccessBlock",
"s3:PutMultiRegionAccessPointPolicy",
"savingsplans:*",
"shield:*",
"sso:*",
"sts:*",
"support:*",
"supportapp:*",
"supportplans:*",
"sustainability:*",
"tag:GetResources",
"tax:*",
"trustedadvisor:*",
"waf:*",
"waf-regional:*",
"wafv2:*"
]

policy_content = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyAllOutsideAllowedRegions"
Effect = "Deny"
NotAction = local.global_services
Resource = "*"
Condition = {
StringNotEquals = {
"aws:RequestedRegion" = var.allowed_regions
}
}
}
]
})
}

resource "aws_organizations_policy" "deny_regions" {
name = var.policy_name
description = "Denies access to AWS services outside of approved regions"
type = "SERVICE_CONTROL_POLICY"
content = local.policy_content
}

resource "aws_organizations_policy_attachment" "attach" {
for_each = toset(var.target_ids)
policy_id = aws_organizations_policy.deny_regions.id
target_id = each.value
}

output "policy_id" {
description = "The ID of the created Service Control Policy"
value = aws_organizations_policy.deny_regions.id
}

output "policy_arn" {
description = "The ARN of the created Service Control Policy"
value = aws_organizations_policy.deny_regions.arn
}

Deploy:

terraform init
terraform apply -var='target_ids=["r-abcd"]' -var='allowed_regions=["us-east-1","us-west-2"]'

Verification

After applying the SCP, verify it is working:

  1. In the AWS Organizations console, navigate to Policies > Service control policies
  2. Select your policy and confirm it shows as attached to the correct targets
  3. Test by attempting to create a resource (e.g., an S3 bucket) in a non-approved region from a member account - it should be denied
CLI verification commands

List all SCPs:

aws organizations list-policies \
--filter SERVICE_CONTROL_POLICY \
--region us-east-1

View policy details:

aws organizations describe-policy \
--policy-id <policy-id> \
--region us-east-1

List targets for a policy:

aws organizations list-targets-for-policy \
--policy-id <policy-id> \
--region us-east-1

Test the restriction (from a member account):

# This should fail if eu-west-1 is not in your allowed regions
aws s3api create-bucket \
--bucket test-bucket-$(date +%s) \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1

Additional Resources

Notes

  • Global services exception: The NotAction list excludes global AWS services (IAM, Route 53, CloudFront, etc.) that operate outside of specific regions. Without these exceptions, the SCP would break essential functionality.

  • Management account immunity: SCPs do not affect the management account itself, only member accounts. Consider additional controls for the management account.

  • Careful testing: Before attaching to your organization root, test the policy on a single non-production account or OU to ensure it does not break legitimate workflows.

  • Region list maintenance: As your organization's needs change, update the aws:RequestedRegion list. New AWS services may also require additions to the NotAction list.

  • SCP inheritance: Policies attached to the root apply to all OUs and accounts. Policies attached to an OU apply to all accounts within it. Plan your attachment strategy accordingly.