Automating Least Privilege in AWS IAM with Policy Sentry

Intro

The Salesforce Security Assurance team would like to share a tool that we recently published called Policy Sentry, which helps to automate the creation of least privilege IAM policies in Amazon Web Services (AWS) environments.

Policy Sentry allows users to create least-privilege IAM policies in a matter of seconds, rather than tediously writing IAM policies by hand. These policies are scoped down according to access levels and resources. In the case of a breach, this helps to limit the blast radius of compromised credentials by only giving IAM principals access to what they need by writing policies according to access levels and resource constraints. Before this tool, it could take hours to craft a secure IAM policy — but now it can take a matter of seconds. This way, developers only have to determine the resources that they need to access, and Policy Sentry abstracts the complexity of IAM policies away from their development processes.

In this post, we’ll walk through the principles of least privilege IAM policies, the general steps that one would use to write them by hand, and show how Policy Sentry automates this process.

Why is this useful?

In a recent breach on an AWS environment, an attacker compromised an internet-exposed WAF appliance server that was vulnerable to Server-Side Request Forgery, obtained AWS credentials via EC2 metadata, and discovered that the server had excessive privileges to access S3 buckets, some of which contained customer data. So the breach likely could have been prevented through one of the two general technical controls:

  • Server-Side Request Forgery¹, which is a difficult problem of its own.

  • Limiting Blast Radius through Least Privilege IAM policies.

Ideally, we want to give users and systems access to only the resources that they need to for their use case, at access levels appropriate to their use case. For instance, accessing a specific S3 bucket (the resource), with “Read” actions only (the access level). This concept is an example of least privilege.

However, until now, it has been challenging to achieve least privilege at scale. One problem is the rapid pace of new features and services by AWS. At the time of writing this blog post, there are now 214 AWS services and over 7,000 different AWS API calls. When we started writing this tool in mid-2019, there were about 180 different AWS services and about 6,000 different API calls. The rapid pace of new feature releases by AWS makes it nearly impossible for a security organization to review and approve new organization-wide IAM policies without disrupting business. The time it takes to write new AWS policies by hand exacerbates this problem. Even for cloud security professionals who are familiar with IAM, writing security-conscious IAM policies by hand can be tedious, inefficient, and time-consuming; and, quite often, security professionals aren’t the ones authoring those IAM policies — developers are! Many Infrastructure as Code developers have experienced something like this:

  • Determined to make your best effort to give users and roles the least amount of privilege you need to perform your duties, you spend way too much time combing through the AWS IAM Documentation on Actions, Resources, and Condition Keys for AWS Services.

  • Your team lead encourages you to build security into your IAM Policies for product quality, but eventually, you get frustrated due to project deadlines.

  • You don’t have a security person assigned to your team day-to-day who can write those IAM policies for you, and there’s no automated tool that will automagically sense the AWS API calls that you perform and then write them for you in a least-privilege manner.

  • After dreaming about that level of automation, you realize that writing least privilege IAM Policies may jeopardize your ability to finish your code in time to meet project deadlines.

  • You use Managed Policies, or you eyeball the names of the API calls and use wildcards instead so you can move on with your life.

Recent breaches have drawn attention to the risks of Server Side Request Forgery (SSRF), but overprivileged IAM policies have continued to be an overlooked issue — partially because of the difficulty in achieving least privilege with IAM at scale. Policy Sentry seeks to lower that difficulty level.

While Policy Sentry can’t determine the context of your IAM role — whether it truly needs access to certain resources based on business or functional requirements — it can restrict access to precisely the resources that you say it needs access to, and do it in a predictable, auditable, and readable manner.

How does it work?

IAM Policy Background

At the most basic level, a policy statement has one or more statements. Each statement always has the following components:

  • Effect: This can be Allow or Deny.
  • Action: A list of AWS IAM Actions.
  • Resource: A list of Amazon Resource Names (ARNs) of various AWS resources.

Consider a statement that has an Allow effect, with the action s3:GetObject and Resources set to *.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "*"
    }
  ]
}

This means that the IAM policy will allow the IAM principal (a role or user) to run s3:GetObject from any S3 bucket in the AWS account. Overly permissive access to S3 buckets - i.e., a wide blast radius - is a cause of many breaches.

Writing Secure Policies by Hand

Now, how could this be prevented with more secure IAM policies? For starters, you could restrict the IAM action to the specific ARN that your role needs access to. With the simple combination of s3:GetObject and a specific S3 bucket — let’s say, arn:aws:s3:::my-bucket/*, the policy is simple enough.*

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

However, real IAM policies quickly become trickier when you combine one IAM action with one IAM resource, as with the above example. Let’s say that your internal customer needs access to the following actions:

  • kms:CreateGrant
  • kms:CreateCustomKeyStore
  • ec2:AuthorizeSecurityGroupEgress
  • ec2:AuthorizeSecurityGroupIngress

To build this policy, we need to navigate to the AWS IAM Documentation — specifically the Actions, Resources, and Condition Keys page for each service.

Let’s take a look at the consolidated EC2 Actions Table below:

ActionsResource
ec2:AuthorizeSecurityGroupIngresssecurity-group*
ec2:AuthorizeSecurityGroupEgresssecurity-group*

As shown in the table above, both of those actions can be restricted to specific security groups — so in the Resources stanza of the IAM policy, we would specify whatever the ARN format of the security-group is.

We can determine that format by navigating to the ARN table on that page. A snippet is below:

Resource TypesARN Format
security-grouparn:${Partition}:ec2:${Region}:${Account}:security-group/${SecurityGroupId}

As you can see above, we’d have to specify the security group ID in the IAM policy.

Let’s see what it would look like in an IAM policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Ec2WriteSecuritygroup",
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupEngress",
                "ec2:AuthorizeSecurityGroupIngress"
            ],
            "Resource": [
                "arn:aws:ec2:${Region}:${Account}:security-group/${SecurityGroupId}"
            ]
        }
    ]
}

Writing Secure Policies based on Resource Constraints and Access Levels

Now, to do this for every single action by hand is very tedious, error-prone, and difficult to audit. Imagine if we had to do this by hand for dozens of IAM actions across multiple services!

Quite often, this leads to one of the following results for Infrastructure as Code developers:

  1. Use Managed IAM Policies, and write the IAM policy later. Sometimes this never gets fixed.

  2. Write the policy correctly, potentially leading to missed project deadlines.

  3. “Eyeball” the IAM policy and use wildcard statements instead of specifying resource ARNs correctly.

Policy Sentry automates this process to avoid that outcome and abstract the complexity of writing IAM policies. As a result, writing secure policies takes seconds instead of hours.

The CRUD mode functionality takes the opinionated approach that IAC developers shouldn’t have to understand the complexities of AWS IAM — we should abstract the complexity for them. In fact, developers should just be able to say…

  • “We need Read, Write, and List access to arn:aws:ssm:us-east-1:123456789012:parameter/myparameter
  • “We need Permissions Management and Tagging access to arn:aws:secretsmanager:us-east-1:123456789012:​secret:mysecret

…and our automation should create policies that correspond to those access levels.

How do we accomplish this? Well, Policy Sentry leverages another column available in the Actions Table from the Actions, Resources, and Condition Keys documentation — the “Access Level” column. See the snippet below.

ActionAccess LevelResource Type
ssm:GetParameterReadparameter
ssm:DescribeParametersListparameter
ssm:PutParameterWriteparameter
secretsmanager:PutResourcePolicyPermissions managementsecret
secretsmanager:TagResourceTaggingsecret

Policy Sentry aggregates all of that documentation into a single database and uses that database to generate policies according to actions, resources, and access levels.

To generate a policy according to resources and access levels, start by creating a template with the create-template command so you can just fill out the fields, rather than memorizing the format. The –name flag specifies the name of the role, the –output-file flag specifies the name of the file, and the –template-type specifies the “mode” that Policy Sentry will use to create policies (crud or actions).

policy_sentry create-template --name myRole --output-file crud.yml --template-type crud

It will generate a file like this:

mode: crud
name: ''
# Specify resource ARNs
read:
- ''
write:
- ''
list:
- ''
tagging:
- ''
permissions-management:
- ''
# Actions that do not support resource constraints
wildcard-only:
  single-actions: # standalone actions
  - ''
  # Service-wide - like 's3' or 'ec2'
  service-read:
  - ''
  service-write:
  - ''
  service-list:
  - ''
  service-tagging:
  - ''
  service-permissions-management:
  - ''
# Skip resource constraint requirements by listing actions here.
skip-resource-constraints:
- ''
# Exclude actions from the output by specifying them here. Accepts wildcards, like kms:Delete*
exclude-actions:
- ''

The template has a few fields:

  • mode - the Policy Sentry “mode”. Acceptable values are crud or actions. CRUD mode creates policies based on access levels constrained to the resource ARNs provided. Read more about Actions mode here.

  • The access levels, read, write, list, tag, and permissions-management. Here, you specify a list of ARNs that your service needs access to, under the proper access level. Policy Sentry will match the ARNs with the ARN formats listed in its database, and will generate a policy that restricts actions at that access level that can be matched to those ARNs only. If any of these fields are not used, you can just delete them from the template.

  • wildcard, a special field where you can specify IAM actions that cannot be restricted to ARNs, like ssm:DescribeParameters, if necessary. Delete this field from the template if it is not used.

Then just paste all of the ARNs under the access levels in the template:

mode: crud
read:
- 'arn:aws:ssm:us-east-1:123456789012:parameter/myparameter'
write:
- 'arn:aws:ssm:us-east-1:123456789012:parameter/myparameter'
list:
- 'arn:aws:ssm:us-east-1:123456789012:parameter/myparameter'
tagging:
- 'arn:aws:secretsmanager:us-east-1:123456789012:​secret:mysecret'
permissions-management:
- 'arn:aws:secretsmanager:us-east-1:123456789012:​secret:mysecret'

Then run the write-policy command. Here, you’ll specify the the Policy Sentry template you just created with–input-file:

policy_sentry write-policy --input-file crud.yml

It will generate these results:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SsmReadParameter",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter",
                "ssm:GetParameterHistory",
                "ssm:GetParameters",
                "ssm:GetParametersByPath",
                "ssm:ListTagsForResource"
            ],
            "Resource": [
                "arn:aws:ssm:us-east-1:123456789012:parameter/myparameter"
            ]
        },
        {
            "Sid": "SsmWriteParameter",
            "Effect": "Allow",
            "Action": [
                "ssm:DeleteParameter",
                "ssm:DeleteParameters",
                "ssm:LabelParameterVersion",
                "ssm:PutParameter"
            ],
            "Resource": [
                "arn:aws:ssm:us-east-1:123456789012:parameter/myparameter"
            ]
        },
        {
            "Sid": "SecretsmanagerPermissionsmanagementSecret",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:DeleteResourcePolicy",
                "secretsmanager:PutResourcePolicy"
            ],
            "Resource": [
                "arn:aws:secretsmanager:us-east-1:123456789012:​secret:mysecret"
            ]
        },
        {
            "Sid": "SecretsmanagerTaggingSecret",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:TagResource",
                "secretsmanager:UntagResource"
            ],
            "Resource": [
                "arn:aws:secretsmanager:us-east-1:123456789012:​secret:mysecret"
            ]
        }
    ]
}

Notice how the policy above recognizes the ARNs that the user supplies, along with the requested access level. For instance, the SID SecretsmanagerTaggingSecret contains Tagging actions that are assigned to the secret resource type only.

How does Policy Sentry compare to other tools?

Policy Sentry is somewhat similar to Trailscraper. Trailscraper queries CloudTrail logs and attempts to “guess” the matching between CloudTrail actions and IAM actions, then generates a policy. Given that there is not a 1-to-1 mapping between the names of Actions listed in CloudTrail log entries and the names AWS IAM Actions, the results are not always accurate. It is a good place to start, but the generated policies all contain Resources: “*”, so it is up to the user to restrict those IAM actions to only the necessary resources. CloudTracker performs similar log-based policy suggestions, and is faster due to the use of Amazon Athena, but it does not generate suggested policies.

RepoKid is a popular tool that was developed by Netflix, and is one of the more mature and battle-tested AWS IAM open source projects. It leverages AWS Access Advisor, which informs you how many AWS services your IAM Principal has access to, and how many of those services it has used in the last X amount of days or months. If you haven’t used a service within the last 30 days, it “repos” your policy, and strips it of the privileges it doesn’t use. It has some advanced features to allow for whitelisting roles and overall is a great tool.

One shortcoming is that AWS IAM Access Advisor only provides details at the service level (ex: S3-wide, or EC2-wide) and not down to the IAM Action level, so the revised policy is not very granular. However, RepoKid plays a unique role in the IAM ecosystem right now in that there are not any open source tools that provide similar functionality. For that reason, it is best to view RepoKid and Policy Sentry as complementary.

We recommend using Policy Sentry to create Identity based policies, using Repokid to revoke out of date policies as your application/roles mature, and never to provision Infrastructure manually — always provision your policies using Infrastructure as Code tools (like Terraform) in a CI/CD pipeline.

Summary

Policy Sentry rapidly speeds up the time to develop IAM policies and ensures that all IAM policies limit access according to the proper CRUD levels, and only to the exact resources that your role needs access to. Before this tool, it could take hours to craft the perfect IAM Policy — but now it can take a matter of seconds. This way, developers only have to determine the resources that they need to access, and Policy Sentry abstracts the complexity of IAM policies away from their development processes.

Be sure to let us know if you have questions about Policy Sentry, if you leverage it in your own environment, or if you have other methods for addressing this problem!

P.S.: We’re hiring!

One of the things that we love about working at Salesforce is that management is committed to automation, including automation of security activities. Certain challenges can only be addressed through careful security engineering and we understand that. If you’re interested in working in Salesforce security, we are hiring in InfraSec and are looking for those with heavy threat modeling skills, secure architecture, and design. If this sounds interesting to you, feel free to follow and message me on Twitter: https://twitter.com/kmcquade3.

¹ There were some updates in late 2019 to the EC2 metadata service to prevent against SSRF attacks, which are covered here — although that requires some re-engineering on the part of AWS customers to support the updates to the EC2 metadata service.

² GitHub: https://github.com/salesforce/policy_sentry/

³ Documentation: https://policy-sentry.readthedocs.io/

⁴ Author: https://twitter.com/kmcquade3

Kinnaird McQuade
Kinnaird McQuade
Founder/CTO

Always remove the french language pack: sudo rm -fr ./*

Related