Dustin WhitedJul 21, 2020 12:00:00 AM10 min read

Analyzing IAM Policies at Scale with Parliament

Analyzing IAM Policies at Scale with Parliament

Modernizing Security: AWS Series - Analyzing IAM Policies at Scale with Parliament

Modernizing Security: AWS Series - Analyzing IAM Policies at Scale with Parliament

For even the most seasoned AWS engineer, configuring Identity and Access Management (IAM) with least privilege can become an endeavor in complex environments. During a breach, an overly permissive policy can result in not only the application’s data being leaked, but it could also allow an attacker horizontal movement into another application, or even an entirely different environment.

IAM policies and roles are a foundational part of every AWS cloud deployment. Eliminating misconfigurations in the environment will help reduce blast radius and decrease attacker effectiveness in the event of a breach. The most efficient and repeatable method for finding these misconfigurations is to automate the detection process using existing libraries. This blog will explore how the Parliament library can be run ad-hoc on a single policy, upon an entire role with multiple attached policies, and leverage a custom detector.

What is Parliament?

Parliament is a tool written in python that lints IAM policies and returns detailed findings based upon misconfigurations in the policy content. It will detect if a policy is malformed, identify unknown permissions that do not exist within the platform, and callout resource mismatches when resources and permissions do not apply to each other.

If you have company-specific security controls and requirements for IAM policies, custom detectors (Private Auditors) can be created to extend Parliament.

{
"Statement": [
{
"Sid": "ElasticComputeCloudFull",
"Action": [
"ec2:*"
],
"Effect": "Allow",
"Resource": [
"*"
]
},
{
"Sid": "RDSFull",
"Action": [
"rds:ModifyDBInstance"
],
"Effect": "Allow",
"Resource": [
"*"
]
},
{
"Sid": "S3Full",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::myprivatebucket"
]
}
],
"Version": "2012-10-17"
}

Let’s explore an example policy: this policy grants the full EC2 service and a single RDS action, ModifyDBInstance on any resource. It’s also scoped down S3 calls to a specific bucket.

This policy has a few problems, one of which is easy to spot. ec2:* grants 399 different permissions. This includes access to DeleteVPC and DeleteNetworkAclEntry.

Sometimes the problems aren’t this obvious, but coming to the table informed about overly-permissive policies and misconfigurations will help implement more secure policies.

This is where Parliament can help.


Parliament as a CLI Tool

First, install Parliament with pip:

pip install parliament

Save the overly permissive policy to a file called policy.json and run it through Parliament with the --file argument:

parliament --file policy.json

The results should look like this:

Raw Parliament Output

 

Raw Parliament Output

Parliament globs wildcard permissions, such as ec2:* into every possible permission included in the wildcard. Each permission is included in the Actions array of the Resource Star finding.

{
"issue": "RESOURCE_STAR",
"title": "Unnecessary use of Resource *",
"severity": "LOW",
"description": "",
"detail": null,
"location": {
"actions": [
"ec2:AcceptTransitGatewayPeeringAttachment",
"ec2:AcceptTransitGatewayVpcAttachment",
"ec2:AcceptVpcEndpointConnections",
"ec2:AcceptVpcPeeringConnection",
"....",
"ec2:TerminateInstances",
"ec2:UpdateSecurityGroupRuleDescriptionsEgress",
"ec2:UpdateSecurityGroupRuleDescriptionsIngress"
],
"sid": "ElasticComputeCloudFull",
"filepath": "policy.json"
}
}
{
"issue": "RESOURCE_STAR",
"title": "Unnecessary use of Resource *",
"severity": "LOW",
"description": "",
"detail": null,
"location": {
"actions": [
"rds:ModifyDBInstance"
],
"sid": "RDSFull",
"filepath": "policy.json"
}
}
{
"issue": "RESOURCE_MISMATCH",
"title": "No resources match for the given action",
"severity": "MEDIUM",
"description": "",
"detail": [
{
"action": "s3:GetObject",
"required_format": "arn:*:s3:::*/*"
}
],
"location": {
"actions": [
"s3:GetObject",
"s3:ListBucket"
],
"sid": "S3Full",
"filepath": "policy.json"
}
}

Using a slightly more condensed version of the output, there are two instances of unnecessary usage of Resource: “*”.

This finding is created when an action is not restricted to any resource, but could be restricted according to the IAM documentation. Parliament retrieves this information from an internal IAM actions json file that is scraped from the various AWS Service’s IAM documentation pages.

Resource mismatch is created when the permission cannot take action on a resource restriction. In this example, s3:GetObject and s3:ListBucket were restricted to a bucket, but the s3:GetObject permission can only act upon object resource types.

The full list of finding types Parliament can generate out of the box is located in the config file.


Parliament as a Python Library

Running Parliament in the command line interface (CLI) gives users the ability to identify issues with IAM policies before creation, and you can scan all policies in an account with the --auth-details-file flag, but what if you wanted to look at specific roles?

Parliament is packaged as a CLI tool, but can also be leveraged as a Python library.


First, create a boto3 connection to get all the policies for a role. The profile and role used will be specific to your environment, but for the sake of example, here it is called ‘dev’ and the role being investigated is ‘test-role’.

Instantiate an empty variable named all_policies to store the policy data. All policy names and content will be stored in this dictionary for analysis.

import boto3
profile_name = 'dev'
role_name = 'test-role'
session = boto3.Session(profile_name=profile_name)
iam = session.client('iam')
def get_policies_for_role(rolename):
all_policies = {}
# two possible types of policies - inline and managed
inline_policies = iam.list_role_policies(RoleName=rolename)['PolicyNames']
attached_policies = iam.list_attached_role_policies(RoleName=rolename)['AttachedPolicies']

There are two different types of IAM policies that could be attached to the role: inline and managed. Each type has a unique boto3 call to list and retrieve the policy name (inline) and ARN (managed). Inline policies use list_role_policies. Managed policies use list_attached_role_policies. The policy identifier is stored in the key PolicyNames for inline policies, and AttachedPolicies for managed policies.

Stored within the inline_policies variable is the list of inline policy names attached to the role, e.g. ['inline-policy', 'instance-policy']

Within the attached_policies variable is the list of PolicyNames and PolicyArns for each managed policy, e.g.[{'PolicyName': 'SecurityAudit', 'PolicyArn': 'arn:aws:iam::aws:policy/SecurityAudit'}]


Now that the policies attached to the role have been identified, the next step is to iterate through the lists and pull the policy documents.

Inline policies are retrieved with get_role_policy. The policy content is stored within the PolicyDocument key.

Managed policies can have different versions. The default version of the policy document is retrieved with get_policy. After retrieving the policy version number, it will be specified in the get_policy_version call.

import boto3
profile_name = 'dev'
role_name = 'test-role'
session = boto3.Session(profile_name=profile_name)
iam = session.client('iam')
def get_policies_for_role(rolename):
all_policies = {}
# two possible types of policies - inline and managed
inline_policies = iam.list_role_policies(RoleName=rolename)['PolicyNames']
attached_policies = iam.list_attached_role_policies(RoleName=rolename)['AttachedPolicies']
# Get each inline policy
for policy in inline_policies:
all_policies[policy] = iam.get_role_policy(
RoleName=rolename,
PolicyName=policy)['PolicyDocument']
# Get the default VersionId, then use that to retrieve that version of the policy.
for policy in attached_policies:
policy_version = iam.get_policy(
PolicyArn=policy['PolicyArn']
)['Policy']['DefaultVersionId']
all_policies[policy['PolicyArn']] = iam.get_policy_version(
PolicyArn=policy['PolicyArn'],
VersionId=policy_version
)['PolicyVersion']['Document']
return all_policies

Finally, each policy document is added to the all_policies dictionary and the function returns that content.

{
InlinePolicyName1: {
PolicyDocument
},
InlinePolicyName2: {
PolicyDocument
},
ManagedPolicyARN1: {
PolicyDocument
}
}

The example above shows the structure of the all_policies dictionary. Policy content is stored under each policy’s name. This will make it easier to report which policy has each finding.


Now that policy documents can be retrieved, let’s create a function to run them through Parliament. At its core, Parliament has a Policy Python class. From this class, policies can be generated and findings identified. Import this class into the script, convert each policy document to the Policy class, and then invoke the analyze() function.

from parliament import Policy
def run_parliament(policy_json, policy_name):
policy = Policy(policy_json)
policy.analyze()

When running this function, you will notice that there is no output from analyze(). The analyze() function stores findings in the Policy class. Formatting will be added based off of the built in Parliament function print_finding. When running this function across many policies at once, adding a field in the finding dictionary called policy_name will identify the location of the finding.

from parliament import Policy
def run_parliament(policy_json, policy_name):
policy = Policy(policy_json)
policy.analyze()
# Assign finding details
findings = [
{
"issue": finding.issue,
"title": finding.title,
"severity": finding.severity,
"description": finding.description,
"detail": finding.detail,
"location": finding.location,
"policy_name": policy_name
}
for finding in policy.findings
]
# Deduplicate the findings for easy output
deduped_findings = []
for f in findings:
if f not in deduped_findings:
deduped_findings.append(f)
return deduped_findings

After creating the findings, a loop deduplicates the findings and only adds it to the new array if the finding does not already exist. Finally, the deduplicated findings are returned to be printed or parsed.

Tie this all together with a check_role function that invokes get_policies_for_role, runs each policy through the run_parliament function, and prints the findings.

import json
def check_role(rolename):
policies = get_policies_for_role(rolename)
# Run each finding through parliament and store the findings in a list
all_findings = [ run_parliament(policy_json, policy_name) for policy_name,policy_json in policies.items() ]
print(json.dumps(all_findings, indent=2))

Run this with check_role(rolename) and it can now parse multiple policies on a single role, and have similar output as the CLI.


Take it to the next level with custom detectors

With the basic framework created for retrieving and analyzing all policy data for a role, it can be extended to detect misconfigurations that are unique to the environment and company standards. Perhaps there is a control that states IAM policies should not have wildcards in the action. Requiring policies specifically enumerate permissions in IAM policies can prevent unexpected permissions being granted to the application.

Reflecting upon ec2:* with its 399 permissions, when AWS introduces a new EC2 permission, it would automatically be granted to the role. The role likely only needs to use a few APIs, but using a wildcard has the potential to allow the role’s permissions to grow exponentially over time.

These wildcards can be detected with a Private Auditor, a separate python file that is integrated into the existing policy analysis. The Private Auditor file can also be used in the CLI version of Parliament.

Create a file called “action_wildcard.py” and then define a function called check_for_wildcard. Raw policy content is retrieved in json format from the Policy class with policy_json.

def check_for_wildcard(policy):
# Make sure Statement is a list
if type(policy.policy_json['Statement']) is dict:
policy.policy_json['Statement'] = [ policy.policy_json['Statement'] ]
for sid in policy.policy_json['Statement']:
if 'Action' in sid:
# Action should be a list for easy iteration
if type(sid['Action']) is str:
sid['Action'] = [ sid['Action'] ]
# Check each action in the list if it has a wildcard, add finding if so.
for action in sid['Action']:
if '*' in action:
policy.add_finding('Action_Wildcard', location={"action": action})

Ensure the Statement is a list of SID(s), and that the Action contained within is also a list. Iterate through the actions and detect if * is in the action and if so, add a finding called Action_Wildcard, and denote the offending action.

Import the private auditor file and invoke check_wildcard(policy) after analyze(). ec2:* has a wildcard in the action; the private auditor found it, and created a finding.

{
"issue": "Action_Wildcard",
"title": "",
"severity": "",
"description": "",
"detail": "",
"location": {
"action": "ec2:*",
"filepath": null
},
"policy_name": "arn:aws:iam::123456789012:policy/test-policy"
}

Detection and Prevention:

Wildcards on action permissions is but one example; private auditors can be extended to find other patterns that may exist in IAM policies. Detection using Parliament private auditors can fit a variety of IAM policy control requirements — explicit resource restriction, ABAC, and even which conditionals exist in a policy. If placed in an IAM deploy pipeline, Parliament could be an effective preventative control, ensuring policies with undesirable content are not created, and providing feedback to developers on how they can be fixed.

A more operationally ready version of the script, which parameterizes role name and profile, can be found here.


Connect with ScaleSec for AWS business

ScaleSec is an APN Consulting Partner guiding AWS customers through security and compliance challenges. ScaleSec’s compliance advisory and implementation services help teams leverage cloud-native services on AWS. We are hiring!

Connect with ScaleSec for AWS business.

RELATED ARTICLES

The information presented in this article is accurate as of 7/20/23. Follow the ScaleSec blog for new articles and updates.