Element 84 Logo

IAM What IAM And That’s All That IAM

10.29.2019

Testing Your IAM Policies

As the complexity of your AWS environment grows, the need for more specific Identity and Access Management (IAM) roles, and policies increases. These IAM policies govern not only the users that are logging in to the AWS environment, but also the permissions that are granted to services, Lambda functions, EC2 instances, and other components that are using resources in your AWS environment. Security best practices dictate that you should only grant the minimum amount of access necessary to perform a task. However, with the growing complexity of the JSON that defines your IAM policies, and the ever changing nature of AWS environments, how do you ensure that your policies are functioning the way you expect?

Since we’re developing our IAM policies as Infrastructure as Code (IaC), it only makes sense to provide automated tests like we would for application code. AWS provides an IAM Policy Simulator that we can use to replicate the results of calling actions on specific resources for a given IAM policy. Let’s see how we can use that to test our IAM policies without actually performing any actions in AWS.

The Test Subject

Here is the CloudFormation template for the role that we need to test. This is a very restricted role that has some basic access to a single S3 bucket, and EC2 access to servers that are tagged as part of the “development” environment:

Resources:
  UserRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: BlogUserRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              AWS:
                - !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action:
              - "sts:AssumeRole"
      Path: /
      Policies:
        -
          PolicyName: "user-permissions"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - s3:ListBucket
                Resource:
                - arn:aws:s3:::my-very-special-test-bucket
              - Effect: Allow
                Action:
                - s3:PutObject
                - s3:GetObject
                - s3:DeleteObject
                Resource:
                - arn:aws:s3:::my-very-special-test-bucket/*
              - Effect: Allow
                Action: ec2:*
                Resource: "*"
                Condition:
                  StringEquals:
                    ec2:ResourceTag/Environment: "development"
              - Effect: Allow
                Action: ec2:Describe*
                Resource: "*"
              - Effect: Deny
                Action:
                - ec2:CreateTags
                - ec2:DeleteTags
                Resource: "*"

Using The IAM Policy Simulator From A Test Case

The IAM Policy Simulator provides two different API methods. simulate_custom_policy will accept a policy as an argument and simulate that policy. simulate_principal_policy accepts a user, group, or role, and will simulate the policies attached to it. The example below uses the latter.

import unittest
import boto3

AWS_ACCOUNT_NUMBER = "xxxxxxxxxxxxxx"

class TestIAMPolicy(unittest.TestCase):

    def setUp(self):
        # Initialize the boto3 IAM client
        self.iam = boto3.client("iam")

        # The ARN of the IAM role that will be tested
        self.policy_source_arn = f'arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/BlogUserRole'

    # Return True if the specified action is allowed on the specified resource
    def allowed(self, response, action, resource):
        evaluation_results = response["EvaluationResults"]
        for result in evaluation_results:
            if action == result["EvalActionName"] and resource == result["EvalResourceName"]:
                return result["EvalDecision"] == "allowed"
        return False

    def test_s3(self):
        resource_arn = "arn:aws:s3:::my-very-special-test-bucket/UsersFile"

        actions = ["s3:GetObject", "s3:PutObject", "s3:PutObjectAcl"]

        response = self.iam.simulate_principal_policy(PolicySourceArn=self.policy_source_arn, ActionNames=actions, ResourceArns=[resource_arn])

        self.assertTrue(self.allowed(response, "s3:GetObject", resource_arn))
        self.assertTrue(self.allowed(response, "s3:PutObject", resource_arn))
        self.assertFalse(self.allowed(response, "s3:PutObjectAcl", resource_arn))

    def test_ec2_without_tag(self):

        actions = ["ec2:StartInstances", "ec2:StopInstances", "ec2:DescribeInstances", "ec2:DeleteTags"]

        response = self.iam.simulate_principal_policy(PolicySourceArn=self.policy_source_arn, ActionNames=actions)

        self.assertFalse(self.allowed(response, "ec2:StartInstances", "*"))
        self.assertFalse(self.allowed(response, "ec2:StopInstances", "*"))
        self.assertTrue(self.allowed(response, "ec2:DescribeInstances", "*"))
        self.assertFalse(self.allowed(response, "ec2:DeleteTags", "*"))

    def test_ec2_with_tag(self):

        actions = ["ec2:StartInstances", "ec2:StopInstances", "ec2:DescribeInstances", "ec2:CreateTags"]
        context_entries = [{"ContextKeyName": "ec2:resourcetag/environment", "ContextKeyValues":["development"], "ContextKeyType": "string"}]

        response = self.iam.simulate_principal_policy(PolicySourceArn=self.policy_source_arn, ActionNames=actions, ContextEntries=context_entries)

        self.assertTrue(self.allowed(response, "ec2:StartInstances", "*"))
        self.assertTrue(self.allowed(response, "ec2:StopInstances", "*"))
        self.assertFalse(self.allowed(response, "ec2:CreateTags", "*"))

if __name__ == '__main__':
    unittest.main()

The boto3 docs have details on what that method returns. The gist of it is that it will return an EvalDecision of allowed for each action for each resource that is specified. For example, the test is checking to see if s3:GetObject is allowed for the ARN of the given S3 bucket. The EC2 simulations are a little more complicated, since it needs to include resource tags, which are specified in the ContextEntries parameter. Remember to test both positive and negative conditions to ensure that policies both grant the expected permissions and deny conditions that shouldn’t be granted.

Summary

The IAM Policy Simulator provided by AWS can be used to simulate complicated IAM policies, and integrate with test frameworks to verify that the policies are granting only the expected permissions. It can be used to not only test that a specific policy grants access to a resource, but can be used to check all the policies attached to a role, group, or user. Another interesting use case is to simulate all of the roles in an account to see which ones have access to a certain resource. With tools like this available, there is no reason to not be testing your Infrastructure as Code.