Skip to main content
Enforcing Preventative Controls in a Multi-Account Environment
Photo by Scott Graham on Unsplash

Enforcing Preventative Controls in a Multi-Account Environment

·1875 words·9 mins
AWS Security Governance Aws Organizations Security Governance Scp Rcp Iam Multi-Account Cloud Governance Preventive Controls
Sergio Cambelo
Author
Sergio Cambelo
Cloud Architect
Table of Contents
This post explores three key AWS Organizations policy types: Service Control Policies (SCPs), Resource Control Policies (RCPs), and Declarative Policies.

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.

Note: This post covers a very specific and advanced-level topic. It is assumed that the reader is familiar with AWS IAM Policies and how they are evaluated.

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.

AWS IAM policy diagram illustrating permissions for launching EC2 instances. On the left, an IAM user has an ‘Allow’ policy granting all actions and resources. However, on the right, a ‘Deny’ policy restricts the ’ec2:RunInstances’ action unless the instance type is ’t2.micro’. Green checkmarks indicate that the user can launch ’t2.micro’ instances, while a red cross shows that launching ’t3.micro’ instances is denied. JSON policy snippets in green (Allow) and red (Deny) are included, with the Deny policy using a ‘StringNotEquals’ condition on ’ec2:InstanceType’

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.

Diagram illustrating AWS S3 access control with IAM policies and SCPs. The image shows two AWS accounts: one within an AWS Organization and a third-party account. In the AWS account, an IAM user with full permissions is denied access to an S3 bucket due to an SCP that explicitly denies all S3 actions. In the third-party AWS account, an IAM user with the same permissions can access the S3 bucket since no SCP restrictions apply.

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.

Diagram illustrating AWS S3 access restrictions due to a Resource Control Policy (RCP). It shows an AWS Organization with an AWS Account containing an S3 bucket and an IAM user. The user has an IAM policy allowing all actions but is denied access due to an RCP that explicitly denies all S3 actions. The diagram also includes a third-party AWS account with an IAM user who is also denied access to the bucket. A Bucket Policy allows access for a specific AWS account, but the RCP overrides it, causing access to be denied.

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.

Diagram illustrating a Declarative Policy in AWS. It represents a VPC with a public and a private subnet. A policy blocks internet access from the public instance through the Internet Gateway (indicated with a red cross), but allows access from the private subnet via a NAT Gateway (indicated with a checkmark). On the right, a JSON block defines the applied policy, including a custom error message.

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
Note: Since this is a connection from outside AWS, no custom error message is triggered by the Declarative Policy.

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

References
#

Related

Manage the AWS Account Root Users of Your Organization Like a Pro
·1341 words·7 mins
AWS Aws Iam Root Scp Organizations
How to Work With IPv6 in AWS and Don't Die in the Process
·1797 words·9 mins
AWS Aws Networking Ipv6
Demystifying AWS KMS key rotation
·2300 words·11 mins
AWS Aws Kms