When operating in multi-cloud environments, it’s important to establish central governance mechanisms to enforce security policies.
Nowadays, it is very common in large organizations to delegate the administration of cloud resources to developers to accelerate value delivery. However, this delegation requires mechanisms that define what a developer can and cannot do.
AWS Organizations and policy types #
To enforce governance at scale, AWS Organizations provides different policy types that help manage access and configuration across AWS accounts. These policies fall into two categories: authorization policies, which define access controls, and management policies, which regulate AWS service configurations.
-
Authorization policies: Manage access for principals and resources.
- Service control policies (SCPs)
- Resource control policies (RCPs)
-
Management policies: Manage the configuration of AWS services.
- Declarative policies
- Backup policies
- Tag policies
- Chat application policies
- AI services opt-out policies
Let’s focus on three key AWS Organizations policy types that help enforce governance at scale: Service Control Policies (SCPs), Resource Control Policies (RCPs), and Declarative Policies.
SCPs #
Service Control Policies (SCPs) help organizations enforce guardrails at the AWS account level by defining permissions for IAM users, roles, and groups.
SCPs do not grant permissions but rather restrict them, ensuring that only approved actions can be executed across accounts.
In the following example, we can see this in more detail.
We have an AWS account with an IAM user that has full admin privileges.
The account has an associated SCP that prevents the launch of EC2 instances of any type other than t2.micro
.
The content of the SCP is as follows:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireMicroInstanceType",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*"
],
"Condition": {
"StringNotEquals": {
"ec2:InstanceType": "t2.micro"
}
}
}
]
}
If the user tries to launch a t2.micro EC2 instance, the operation will succeed. However, on the contrary, if the user tries to launch a t3.micro EC2 instance, the operation will fail with an error message like this:
An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform this operation. User: arn:aws:sts::440473545235:assumed-role/AWSReservedSSO_AdministratorAccess_06f46bi3895578b6/user is not authorized to perform: ec2:RunInstances on resource: arn:aws:ec2:eu-west-1:440473545235:instance/* with an explicit deny in a service control policy.
One of the key characteristics of SCPs is that they only affect principals belonging to the organization and have no effect on principals from outside the organization.
Let’s explore this in more detail with an example.
A user from an AWS account within the organization has a policy with full admin permissions attached. However, the AWS account also has an SCP applied, which looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::*"
]
}
]
}
If this user tries to access the bucket, they will receive an “Access Denied” error like this:
% aws s3 ls terraform-20250330204504838800000001
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:sts::440473545235:assumed-role/AWSReservedSSO_AdministratorAccess_06f46be2876578b6/user is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::terraform-20250330204504838800000001" with an explicit deny in a service control policy
In this case, the SCP affects the principal that belongs to the account where the SCP is attached.
However, if the principal belongs to another AWS account that is not affected by this SCP (or even a third-party account outside the AWS Organization), it will not be impacted by this restriction.
Assuming the S3 bucket has a bucket policy that grants access to this external principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::964937560139:root"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::terraform-20250330204504838800000001/*",
"arn:aws:s3:::terraform-20250330204504838800000001"
]
}
]
}
If we try the same operation as above, we can see that the user is not affected by the SCP and can retrieve the bucket contents.
% aws s3 ls terraform-20250330204504838800000001
2025-03-30 22:48:25 36844 scp-ec2.webp
As we can see, SCPs are not enough to centrally enforce management controls for this kind of situation. Luckily for us, in these cases, we can use another centrally managed control: Resource Control Policies (RCPs).
RCPs #
Resource Control Policies (RCPs) define who can access specific AWS resources and under what conditions. Unlike SCPs, which apply at the principal level, RCPs enforce security policies directly on AWS services and resources.
Using the previous example as a starting point, let’s see how we could effectively restrict access to S3 centrally, even though the bucket has a permissive policy.
In this case, instead of applying an SCP, we applied an RCP to the Bucket Account, which denies all S3 operations.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::*"
}
]
}
With this policy enforced, we repeat our operation. First, we try with the User within the Organization’s Account:
aws s3 ls terraform-20250330204504838800000001
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:sts::442042545235:assumed-role/AWSReservedSSO_AdministratorAccess_06f46be2876578b6/user is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::terraform-20250330204504838800000001" with an explicit deny in a resource control policy
As we can see, the result is the expected Access Denied error.
Now, we try the same operation with our external user:
% aws s3 ls terraform-20250330204504838800000001
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
This time, with the RCP applied, the user no longer has access to the bucket, despite the Bucket Policy granting permission.
It is worth noting that when accessing the bucket with external users, the error message is less verbose compared to when an Account user encounters it.
So far, we have seen how to centrally enforce access controls to different AWS services, but none of these controls have altered or conditioned the configuration of the services themselves. Also, we have seen that the error message returned when the access is denied does not offer the user any information on why this happened.
Let’s see how we can tackle these problems with the use of a third type of centrally managed control: Declarative Policies.
Declarative Policies #
Declarative Policies allow organizations to define desired configurations for AWS services at scale. These policies enforce best practices without requiring manual intervention, ensuring compliance with security and operational standards.
Declarative policies are enforced at the service control plane. So instead of determining who can access or not access the service, they persist as a limit on some features of the service. We will see this in more detail later with an example.
This policy type, such as SPCs and RCPs, can be applied at different levels of the organization: Root, OU, or account.
At the moment of writing this post, Declarative Policies only support the EC2 service. The available attributes are the following:
AWS service | Attribute | Policy effect |
---|---|---|
Amazon VPC | VPC Block Public Access | Controls if resources in Amazon VPCs and subnets can reach the internet through internet gateways (IGWs). |
Amazon EC2 | Serial Console Access | Controls if the EC2 serial console is accessible. |
Amazon EC2 | Image Block Public Access | Controls if Amazon Machine Images (AMIs) are publicly sharable. |
Amazon EC2 | Allowed Images Settings | Controls the discovery and use of Amazon Machine Images (AMI) in Amazon EC2 with Allowed AMIs. |
Amazon EC2 | Instance Metadata Defaults | Controls IMDS defaults for all new EC2 instances launches. |
Amazon EBS | Snapshot Block Public Access | Controls if Amazon EBS snapshots are publicly accessible. |
Custom error messages #
Other key differences with other policy types are the possibility to define custom error messages for users. This could be useful to provide URLs pointing to documentation, instructing the user on how to properly configure the service.
Declarative Policies and Service-Linked Roles #
Another limitation of SCPs and RCPs is that they do not apply to any Service-Linked Role. These are roles that are associated directly with some AWS services and are fully managed by AWS.
With Declarative Policies, Service-Linked Roles are also subject to the policies enforced on the service, providing an extra layer of security.
Declarative Policies Example #
Once we have defined what Declarative Policies are and their key characteristics, let’s see all this in action with an example.
We have attached the following declarative policy to our AWS account:
{
"ec2_attributes": {
"vpc_block_public_access": {
"internet_gateway_block": {
"mode": {
"@@assign": "block_ingress"
},
"exclusions_allowed": {
"@@assign": "disabled"
}
}
},
"exception_message": {
"@@assign": "This is an example of a custom error message."
}
}
}
With this policy attached, all inbound traffic through the Internet Gateway will be blocked. As a result, the EC2 instance in the public subnet won’t be accessible, even if it has an ENI with a public IP, a permissive Security Group, and an NACL.
If we try to reach the Nginx deployed on this instance, we will see that the instance does not respond, and after a while, we receive the following error.
% curl -I http://108.129.156.193
curl: (55) Send failure: Broken pipe
If we temporarily detach the Declarative Policy from the account and repeat the same operation, we observe that Nginx can be reached.
sergio@K243 ~ % curl -I http://108.129.156.193
HTTP/1.1 200 OK
Server: nginx/1.26.3
Date: Thu, 03 Apr 2025 16:46:19 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 11 Feb 2025 02:00:47 GMT
Connection: keep-alive
ETag: "67aaaf4f-267"
Accept-Ranges: bytes
On the other hand, since this policy only blocks ingress traffic, the EC2 instance in the private subnet will still be able to reach the internet through the NAT Gateway in the public subnet.
From our private EC2 instance, we make a request to cambelo.com and see that we can reach it without any problems.
sh-5.2$ curl -I https://cambelo.com
HTTP/2 200
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000
cache-control: public, max-age=0, must-revalidate
content-type: text/html; charset=utf-8
etag: "d8q1r0f8k5cazq1"
last-modified: Wed, 26 Mar 2025 08:15:14 GMT
server: Caddy
vary: Accept-Encoding
x-content-type-options: nosniff
content-length: 46297
date: Thu, 03 Apr 2025 16:34:52 GMT
Wrapping up #
In this post, we have seen three different policy types we can use to centrally enforce governance controls across our organization.
SPCs control what permissions principals have to access resources, RCPs define the permissions a given resource can have, and Declarative Policies enforce limits directly at the service control plane.
Here is a summary table to compare the key characteristics of each policy type:
Service control policies | Resource control policies | Declarative Policies | |
---|---|---|---|
Purpose | Enforce consistent access controls on principals at scale | Enforce consistent access controls on resources at scale | Enforce default service configuration at scale |
Mechanism | By controlling permissions of principals at an API level | By controlling permissions for resources at an API level | By declaring the desired outcome (not at an API level) |
Applied via | IAM / Auth implementation | IAM / Auth implementation | Service control plane implementation |
Feedback | Auth access denied | Auth access denied | Configurable error per policy |
Affects SLRs? | No | No | Yes |
Example | Deny access to unapproved regions | Only trusted identities can access my resources | Configure Block Public Access for AMIs |
Enjoyed this article? Subscribe to the newsletter and be the first to know when a new one is published!
Subscribe