Granularity levels in AWS IAM policies

What are the controls available to fine-tune policies

Author's image
Tamás Sallai
6 mins

When everything is allowed for everybody in an account it's great for usability but horrible for security. When everything is denied then the account is completely secure but useless. The sweet spot is in the middle, where everything that is needed is allowed but everything else is denied. This is called the least privilege.

The main control over what can be done in an account is via IAM policies. They can allow and restrict users based on different elements in the policy statement.

In this article, we'll look into the granularity levels of how you can define permissions. Starting from the most coarse "allow everything" down to the finest details, such as "allow getting a specific object from an S3 bucket when the user is MFA authenticated".

AWS managed policies

AWS provides a set of policies that follow the one-size-fits-all approach. They are suitable for all accounts, but they are coarsest.

The best thing about AWS managed policies is that you don't need to think about policy structure or even write any JSON. Just select an existing policy available for all accounts, attach it to a users/groups/roles and they get the permissions.

Attach managed policies to a user

Several policies are available, ranging from the "allow everything" level down to "allow read/write to individual services".

AdministratorAccess

The jolly joker, this policy allows everything that can be allowed for an IAM user. Adding this to a user gives it free roam across the account.

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

PowerUserAccess

Similar to the AdministratorAccess, but it does not grant access to IAM.

{
	"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": "*"
		}
	]
}

Service-based

You can selectively allow access to individual services using these service-based AWS managed policies. If you know a user only needs access to S3, then you can attach the AmazonS3FullAccess policy that allows everything inside S3 but does not grant allow using any other service.

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

The IAMFullAccess is an exception. With it, the user can change its own permissions, making this managed policy effectively equivalent to the admin access.

Read-only access

To further restrict service-level access, you can use the ReadOnlyAccess policy versions. These allow only reading data and resource states but does not allow any modification.

For example, the AmazonS3ReadOnlyAccess allows Listing and Getting things inside S3:

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

Custom policies

AWS managed policies offer granularity only up to this point and to specify permissions in more detail you need to write custom policies. It's a more involved process, but it also unlocks greater control over what is allowed.

The IAM console provides an editor to help set up things. There is a visual editor where you can select the services, actions, resources, and conditions and it offers some context-sensitive help and links to the relevant docs.

IAM policy visual editor

Or, alternatively, you can edit the JSON manually:

IAM policy JSON editor

Here's the general structure of a policy:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": "...",
			"Resource": "...",
			"Condition": {
				...
			}
		}
	]
}

Policies are made of statements. In each statement, there is an Effect that is either Allow or Deny whether the statement grants or restricts permissions.

All the other parts of the statement are filters. They define which requests are matched when IAM decides whether to allow or deny an action.

Actions

Actions define what the user is doing, such as listing an S3 bucket, terminating an EC2 instance, or creating a new IAM user. By specifying individual actions you can restrict what is allowed beyond the all/read-only levels.

For example, S3 defines the s3:GetObject and the s3:ListBucket actions, the first allowing downloading objects, while the latter getting the list of objects. This is more granular than read-only access as it does not allow reading the bucket policy among other things.

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

Resources

The Resource element scopes down the permissions to individual resources. All the above statements allowed access to all buckets. To allow listing and downloading from a single bucket, specify its ARN:

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

The resource type is dependent on the action, such as s3:ListBucket works on the bucket-level while s3:GetObject on the object-level. The above statement specifies 2 actions and 2 resources, but since they need different resource types they are two separate action-resource pairs.

To illustrate this, let's separate the two:

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

Consult the IAM reference to see what are the ARN formats for the resources, and actions table for which actions require what resource type.

You can further restrict the applicable resources by using subresources. For example, you can specify individual objects inside an S3 bucket, or use wildcards to specify a group of them. This policy allows reading objects from the public folder:

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

Conditions

Conditions provide filters on the request context, such as the time of the request, whether the user authenticated with MFA or the resource has the given tag.

To illustrate this, this policy allows downloading objects only when the user used an MFA device to log in:

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

Another example is to allow downloading objects that are tagged with readable:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::bucket/*",
			"Principal": "*",
			"Condition": {
				"StringEquals": {
					"s3:ExistingObjectTag/readable": "true"
				}
			}
		}
	]
}

Conditions are highly dependent on the operation as some support many things to filter on, some don't. The IAM reference provides a good starting point to see what conditions are available, and there are many guides in the docs to show how to achieve fine-grained policies.

Adding conditions to statements open a lot of possibilities but they are frustrating to write. There is no way of debugging when something is not working as expected, so you can only guess the values of the condition elements through trial-and-error.

Conclusion

When you give access to the account you can use AWS managed policies for coarse-grained access control. These range from the "everything included" to read/write access to individual services.

If you want more granular policies, you need to write them yourself, either using the visual editor or manually writing the JSON. With custom policies, you can define individual actions and resources, and you can also add conditions to the request context.

October 6, 2020

Free PDF guide

Sign up to our newsletter and download the "Git Tips and Tricks" guide.


In this article