ABAC vs RBAC – Leveraging AWS IAM to simplify large-scale access control

Tobias Quitzau
9. May 2023
Reading time: 7 min
ABAC vs RBAC – Leveraging AWS IAM to simplify large-scale access control

Introduction

Access Control is becoming increasingly complicated and cumbersome when organizations get larger, operate more services and generally get more users, which naturally comes with a fluctuating user base. Therefore, it’s very important for large-scale organizations to look into ways to simplify the management of Access Control.

There are several ways to achieve this in AWS IAM—in this article, we will take a look at a principle you can apply to your policies so that you aren’t changing these every time you have a new employee gaining access or a new project that your users start working on. The main two approaches to policies are RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control)—firstly we will take a look at both of these, so we can understand their upsides, and then we will quickly see which one is more scalable.

What is RBAC?

RBAC is based on how organizations are usually structured, which makes it quite easy to understand and implement. It stands on the idea that each employee has a role assigned to them—there are developers, admins, managers and so on. These roles define what the person is allowed to do in AWS—Amazon also provides a few policies based on distinct job roles as managed policies. For example, here is the AWS-provided policy PowerUserAccess which is intended for developers.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "NotAction": [
                "iam:*",
                "organizations:*",
                "account:*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "iam:DeleteServiceLinkedRole",
                "iam:ListRoles",
                "organizations:DescribeOrganization",
                "account:ListRegions"
            ],
            "Resource": "*"
        }
    ]
}

As we can see here, it’s granting permissions on all resources that the allowed actions can be performed on. The principle of least privilege is something we should all follow, as every additional permission a user has can be considered a vulnerability, but that is very difficult to scale. What if we need to give a developer access to a certain database for testing and an S3 bucket for retrieving files? A policy for that could look like this.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow everything on the testing-database-1",
      "Action": "rds:*",
      "Effect": "Allow",
      "Resource": "arn:aws:rds:us-east-2:123456789012:db:testing-database-1"
    },
    {
      "Sid": "Allow to get objects from the testing-bucket",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::testing-bucket"
    }
  ]
}

Now our developer requests for access to an additional S3 bucket, a DynamoDB table and a few Lambda functions with a day in between each of these requests, and we always go back to the policy. Imagine this workflow with hundreds of users who all need frequent access to other resources. Editing policies so that other people can get working on a resource will become an essential part of your everyday work. It quickly becomes clear that this is not scalable at all.

Introducing a solution: ABAC

One approach to dynamically scaling IAM policies as resources grow is to use Attribute-Based Access Control (ABAC). The general principle of ABAC is to not explicitly specify which resources a principal is allowed to access, but to instead define a filter condition that determines which resources access is granted to. These filter conditions can be defined based on a wide range of attributes, such as tags (which are the most common and customizable option), EC2 instance type, or S3 prefixes, among others.

This is the second model mentioned in the introduction. To better understand this approach, let’s examine one of our policies from earlier, but this time with the added attribute filtering.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow everything on RDS databases of their developer team",
      "Action": "rds:*",
      "Effect": "Allow",
      "Resource": ",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/developer-team": "${aws:PrincipalTag/developer-team}"
        }
      }
    },
    {
      "Sid": "Allow to get objects from the buckets of their developer team",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/developer-team": "${aws:PrincipalTag/developer-team}"
        }
      }
    }
  ]
}

With a Condition for each statement in our policy, there’s no need to specify a specific resource using an ARN, as the policy applies to all resources that match the checked tag through the StringEquals expression. The value of the tag key being checked is associated with the principal that calls the action.

By using a policy like this, we can address the scalability issue by allowing the policy to adapt to an expanding set of resources, teams, and users. However, the next challenge is to ensure that these policies are secure. With the current policy, it’s still possible for a different role with the ability to change tags on a resource to either restrict a developer’s access to a resource or grant access to a resource that was not intended for them.

Mandatory and secure tagging

To ensure the security of scalable ABAC policies, two important steps must be taken: requiring tags and protecting these tags. First, let’s examine what a statement would look like that requires tags upon the creation of a resource. This statement should be added to our policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Require the developer-team tag on RDS resource creation",
      "Action": "rds:Create*",
      "Effect": "Allow",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestTag/developer-team": "${aws:PrincipalTag/developer-team}",
        "ForAllValues:StringEquals": {
          "aws:TagKeys": [
            "developer-team",
            "application",
            "project"
          ]
        }
        }
      }
    }
  ]
}

When examining environment variables in IAM, such as aws:RequestTag, it’s important to consider the variable’s position in the chain. In this case, the RequestTag variable is obviously used when a request is sent to an AWS API, which may include the AWS Management Console, an SDK, or the CLI. In this policy, we require that the developer-team tag be set to the same value as the Principal issuing the request; otherwise, the request will fail. Additionally, we provide the policy with a list of aws:TagKeys that can be set. Any keys not in this list cannot be set when creating a resource in RDS.

With the policy in place, our developers can only create a resource if they set a specific tag to the correct value. However, what if a developer creates a resource and then changes the value of that key to something else? To prevent this scenario, we can include statements like the following in our policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Deny tag removal when the tag developer-team is set on a resource",
      "Action": "rds:RemoveTagsFromResource",
      "Effect": "Deny",
      "Resource": "*",
      "Condition": {
        "Null": {
          "aws:ResourceTag/developer-team": "false"
        }
      }
    }
    {
      "Sid": "Deny modification of RDS resource when the wrong developer-team value is sent in the request",
      "Action": "rds:Modify*",
      "Effect": "Deny",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestTag/developer-team:": "${aws:PrincipalTag/developer-team}"
        }
      }
    }
  ]
}

The first statement prevents the requester from removing tags from a resource when our team tag is set. The Null condition expression checks whether a value exists or not. The second statement denies any modification to any RDS resource when the request includes a value in our team tag key other than the expected value, which should be the same value attached to the calling principal.

By implementing additional statements to make our scalable ABAC policy more reliable and secure, we no longer have to manually update our policy every time our developer teams create new resources. Everything is now scaling dynamically with the progress our teams make.

Conclusion

Both RBAC and ABAC are feasible access control models for AWS IAM. However, for larger organizations with complex access control needs, ABAC offers a superior level of scalability and flexibility. The greater flexibility provided by ABAC enables organizations to implement a more detailed approach to access control, which in turn can aid in preventing security breaches and minimizing the likelihood of data loss.

However, it is important to note that ABAC can be more complex and requires a deeper understanding of the various attributes involved in access control decisions. It also requires more work upfront, as administrators must carefully consider and manage the various attributes involved in access control decisions. Ultimately, the best choice between RBAC and ABAC will depend on the specific requirements of the organization, and a thorough evaluation of each model should be conducted before making a decision.