AWS Organizations Service Control Policies
Example Policy to restrict access based on requested region:
https://asecure.cloud/a/scp_whitelist_region/
Note: The terminology in the linked article says “whitelist” but the preferred term set is “allow-list” and “block-list
Approach
Learned! You can only call this from the actual org root account, not child accounts! Regardless, this is what a policy would look like:
Name: ListExperimentalAccountSCPs
aws organizations list-policies-for-target --filter SERVICE_CONTROL_POLICY --target-id <account_id>
Starting Policy to list Account SCPs:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:ListPoliciesForTarget",
"Resource": "arn:aws:organizations::*:account/o-*/*"
}
]
}
Example Response:
This shows the default policy added to any account created in an organization with SCP features enabled. Or accounts that are invited to an organization.
{
"Policies": [
{
"Id": "p-FullAWSAccess",
"Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
"Name": "FullAWSAccess",
"Description": "Allows access to every operation",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": true
}
]
}
Why is the above important? When we attach/detach policies, there is an important restriction with the second step. You cannot remove the last remaining policy.
Why would that matter? If the account has NO policies and you attached this region blocking one, then detach it, you are then removing the last policy. This is not likely to happen here, so let’s move on!
The SCP policy, from the example. We will leave two regions allow-listed, but adjust them slightly later.
{
"Version": "2012-10-17",
"Statement": [
{
"NotAction": [
"a4b:*",
"acm:*",
"aws-marketplace-management:*",
"aws-marketplace:*",
"aws-portal:*",
"awsbillingconsole:*",
"budgets:*",
"ce:*",
"chime:*",
"cloudfront:*",
"config:*",
"cur:*",
"directconnect:*",
"ec2:DescribeRegions",
"ec2:DescribeTransitGateways",
"ec2:DescribeVpnGateways",
"fms:*",
"globalaccelerator:*",
"health:*",
"iam:*",
"importexport:*",
"kms:*",
"mobileanalytics:*",
"networkmanager:*",
"organizations:*",
"pricing:*",
"route53:*",
"route53domains:*",
"s3:GetAccountPublic*",
"s3:ListAllMyBuckets",
"s3:PutAccountPublic*",
"shield:*",
"sts:*",
"support:*",
"trustedadvisor:*",
"waf-regional:*",
"waf:*",
"wafv2:*",
"wellarchitected:*"
],
"Resource": "*",
"Effect": "Deny",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-2", "us-west-1"]
}
}
}
]
}
Command to create policy from file above, named policy.json
:
aws organizations create-policy --name AllowListRegion --type SERVICE_CONTROL_POLICY --description "Allows operation in only a region(s)" --content file://policy.json
IAM Policy to allow creation of a SCP:
Name: CreateSCP
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:CreatePolicy",
"Resource": "*"
}
]
}
If this works correctly, there should be some json output in the terminal, with the policy json-stringified in the “content” spot. It’s visually a hot mess, so I won’t put it in here.
Now we have the allow-list policy for further use. The next step is to attach the restriction to an account. In our case, the new experimental account we created for this experiment.
This will require the policy id. To retrieve this, another set of permissions are requried.
Command to list policies:
aws organizations list-policies --filter SERVICE_CONTROL_POLICY
Permissions Required:
Name: ListOrgSCPs
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:ListPolicies",
"Resource": "*"
}
]
}
Response (org ID and target account number redacted)
{
"Policies": [
{
"Id": "p-FullAWSAccess",
"Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
"Name": "FullAWSAccess",
"Description": "Allows access to every operation",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": true
},
{
"Id": "p-inro61w0",
"Arn": "arn:aws:organizations::<account_id>:policy/<organization_id>/service_control_policy/p-inro61w0",
"Name": "AllowListRegion",
"Description": "Explicity allows operation in only a region(s)",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": false
},
{
"Id": "p-t4wzso4u",
"Arn": "arn:aws:organizations::<account_id>:policy/<organization_id>/service_control_policy/p-t4wzso4u",
"Name": "Quarantine",
"Description": "Deny creating any infrastructure or services",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": false
}
]
}
Our “AllowListRegion” policy is what we are after, so note that "Id"
in the output. In our case it’s p-inro61w0
arn:aws:organizations::716374413161:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0
Command to attach policy
aws organizations attach-policy --policy-id p-inro61w0 --target-id <account_number>
Permissions required
Name: AttachSCP
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:AttachPolicy",
"Resource": [
"arn:aws:organizations::*:account/o-*/*",
"arn:aws:organizations::716374413161:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0"
]
}
]
}
If successful, there will be no errors, but there won’t be any output, either. That’s okay, we can use an earlier command to verify if that is attached to our experimental account
aws organizations list-policies-for-target --filter SERVICE_CONTROL_POLICY --target-id <account_id>
Instead of just the one, default policy, there should now be 2 policies listed, and more precisely, our AllowListRegion policy:
{
"Policies": [
{
"Id": "p-inro61w0",
"Arn": "arn:aws:organizations::<account_id>:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0",
"Name": "AllowListRegion",
"Description": "Explicity allows operation in only a region(s)",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": false
},
{
"Id": "p-FullAWSAccess",
"Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
"Name": "FullAWSAccess",
"Description": "Allows access to every operation",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": true
}
]
}
Now we need to make sure we have permissions to detach the policy from the account for when we want to restore back to using our region that we blocked above.
aws organizations detach-policy --policy-id p-inro61w0 --target-id <account_id>
Name: DetachSCP
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:DetachPolicy",
"Resource": [
"arn:aws:organizations::*:account/o-*/*",
"arn:aws:organizations::716374413161:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0"
]
}
]
}
Similar to attaching and verifying, we can detach and verify that we no longer have this policy and are back to just the default policy
{
"Policies": [
{
"Id": "p-FullAWSAccess",
"Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
"Name": "FullAWSAccess",
"Description": "Allows access to every operation",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": true
}
]
}
We know there will be some experimentation with regions on, regions off. But sometimes your desired region choices change. Either due to pricing, feature availability, or latency. Let’s make sure we can change our allow-listed “DR” region in that event.
For this example, we will just change from allowing us-west-1, us-east-2
to allowing us-west-2, us-east-2
aws organizations update-policy --policy-id p-inro61w0 --content file://policy.json
Name: UpdateSCP
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "organizations:UpdatePolicy",
"Resource": "arn:aws:organizations::716374413161:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0"
}
]
}
Similar to creating an SCP, successfully updating one will spit back the updated policy, and the json-stringified policy contents. We will omit that here, because it’s a mess.
If you want to get a bare-bones nginx instance running, this article is my goto https://www.nginx.com/blog/setting-up-nginx/
But we really just want to see if we can describe instances before and after region blocking.
Create a tiny ec2 instance in the experimental account in the primary (us-east-2) and secondary (us-west-2) regions.
Do note, we are going to dramatically reduce the output of these commands by parsing with a tool called jq
. This is a reference you can follow for quick syntax examples
https://blog.scottlowe.org/2018/05/23/quick-post-parsing-aws-instance-data-with-jq/
List instance in us-east-2 (remember to assume a role in the experimental account, not the master account)
aws ec2 describe-instances --region us-east-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
{
"ID": "i-0b5a6563865e097da"
}
List instance in us-west-2 (remember to assume a role in the experimental account, not the master account)
aws ec2 describe-instances --region us-west-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
{
"ID": "i-0be1ea7c43f78a323"
}
At this point, feel free to stop the running instances, we aren’t requiring them to be running (and charging you money) to complete the experiment.
Do note: You must terminate these to avoid all charges. The basic 8gb of EBS storage for each of the 2 instances does have a nominal charge, on a recurring monthly basis!
Stopped, unused, instances are an insidious place to leak money.
Hypothesis:
We can list from secondary (us-west-2) We cannot list from primary (us-east-2)
Updated policy blocking primary (us-east-2)
{
"Version": "2012-10-17",
"Statement": [
{
"NotAction": [
"a4b:*",
"acm:*",
"aws-marketplace-management:*",
"aws-marketplace:*",
"aws-portal:*",
"awsbillingconsole:*",
"budgets:*",
"ce:*",
"chime:*",
"cloudfront:*",
"config:*",
"cur:*",
"directconnect:*",
"ec2:DescribeRegions",
"ec2:DescribeTransitGateways",
"ec2:DescribeVpnGateways",
"fms:*",
"globalaccelerator:*",
"health:*",
"iam:*",
"importexport:*",
"kms:*",
"mobileanalytics:*",
"networkmanager:*",
"organizations:*",
"pricing:*",
"route53:*",
"route53domains:*",
"s3:GetAccountPublic*",
"s3:ListAllMyBuckets",
"s3:PutAccountPublic*",
"shield:*",
"sts:*",
"support:*",
"trustedadvisor:*",
"waf-regional:*",
"waf:*",
"wafv2:*",
"wellarchitected:*"
],
"Resource": "*",
"Effect": "Deny",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-west-2"]
}
}
}
]
}
Attach the policy (execute this from the limited user in the management account)
aws organizations attach-policy --policy-id p-inro61w0 --target-id <account_number>
Verify the attached policy
aws organizations list-policies-for-target --filter SERVICE_CONTROL_POLICY --target-id <account_id>
{
"Policies": [
{
"Id": "p-inro61w0",
"Arn": "arn:aws:organizations::<master_account_number>:policy/o-6lhxkkma2d/service_control_policy/p-inro61w0",
"Name": "AllowListRegion",
"Description": "Explicity allows operation in only a region(s)",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": false
},
{
"Id": "p-FullAWSAccess",
"Arn": "arn:aws:organizations::aws:policy/service_control_policy/p-FullAWSAccess",
"Name": "FullAWSAccess",
"Description": "Allows access to every operation",
"Type": "SERVICE_CONTROL_POLICY",
"AwsManaged": true
}
]
}
From the child account, now try to list the instances in us-east-2
$ > aws ec2 describe-instances --region us-east-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.
And then from us-west-2
$ > aws ec2 describe-instances --region us-west-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
{
"ID": "i-0be1ea7c43f78a323"
}
So far, our experiment is successful! Even an administrator role in the child account, with full stars access, can no longer even issue that command!
Detach the policy (from the limited user, in the management account)
aws organizations detach-policy --policy-id p-inro61w0 --target-id <account_id>
Validate (from the child account, that instances in both regions can again be described)
Primary
$ > aws ec2 describe-instances --region us-east-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
{
"ID": "i-0b5a6563865e097da"
}
Secondary
$ > aws ec2 describe-instances --region us-west-2 | jq '.Reservations[] | .Instances[] | {ID: .InstanceId}'
{
"ID": "i-0be1ea7c43f78a323"
}
We have tested our hypothesis that this SCP will be able to shut down almost all access to resources, via IAM. This means that anything relying on API calls to AWS itself will be blocked.
This would be API gateways invoking lambdas, operations with dynamo, interaction with queues, etc. All the basics needed to power a modern app. Shut down immediately.
Fascinating stuff!
To save money:
Terminate EC2 instances (don’t forget any key-pairs, if you created them)
These things are handy to have around for further experimentation, but can safely be deleted for this exercise: