AWS IAM deep dive: Identity-based, and Resource-based policies
How to grant and deny access in an AWS account
The IAM policy is the cornerstone of security of an AWS account. There is an oft-repeated notion of the shared responsibility model that stipulates that certain parts of security is the responsibility of the cloud provider, such as preventing malicious parties physical access to the infrastructure. On the other hand, the cloud provider gives you security controls and it is your responsibility to use them properly.
The main security control is IAM and it is used everywhere inside AWS. With IAM you are not only able to create users for people who need access to the account but also to define what each user has access to.
There are several types of policies, but the two most important ones are the identity-, and the resource-based policies. Whatever you do in your AWS account you'll need to use these.
Identity-based policies
Identity-based policies are the easier of the two, as some variation of it is present in most systems. They define what an identity can do, such as a user has access to an S3 bucket.
There are two types of identities in AWS: users and roles. By attaching a policy to an identity you can give it permissions to access resources. For example, you can attach this policy to allow a user to read an object in an S3 bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::<bucket>/text.txt"
}
]
}
Since the policy is attached to an identity, a user in this case, there is no Principal element in the policy. On the other hand, the Resource defines the object in the bucket that the user can access.
Note that groups are not identities but a way to attach policies to multiple users.
Users
There is only one type of user that can be created, but depending on how they are used we can distinguish natural and technical users.
Natural users are created for people. Bob, who is responsible for the S3 buckets, gets a user with the permissions needed to do his work. These type of users usually have a password to sign in to the console and sometimes access keys to allow programmatic access to the account. Since the user is created and managed by a single person, that person is responsible for it. Also, it's likely that multiple people have the same job, so permissions are usually attached to groups instead of the users themselves.
Technical users allow other services access to resources inside the account. For example, a web application hosted outside of AWS where users can send notifications to an SNS topic. In this case, you can create a new user, generate an access key and include that in the web application's backend. Then you attach a policy so that it can publish a notification to the topic.
Roles
Roles are temporary credentials that other identities (users and roles) can assume to gain access to the permissions the role has. For example, when a bucket can only be accessed by a role then if a user can assume that role he then can access the bucket indirectly.
There are many use-cases for roles inside AWS. To give access to your account, you can use a role that can be used by identities in another account. This way you don't need to create users and transfer secrets (passwords or access keys) to grant cross-account access. Another use-case is users logging in via SAML or Cognito. Instead of creating a user for every single person who can log in, you define a role and all users assume it. Then you can give permissions to the role and all users signed in this way will gain those permissions.
Also, AWS services use roles to gain permissions to access your account. For example, CloudTrail can put logs into a CloudWatch Log group, but it needs permission to do so. On the Trail config, you need to specify a role:
This role allows the CloudTrail service to assume it:
And the role has the policies to access CloudWatch Logs:
This way you have full control over what a given service can do. And this pattern appears in many places. A Lambda function uses a role in a similar way, the S3 bucket replication gains permissions to write a bucket using a role, and AWS Organizations creates a role in the member account so that you can initialize the resources inside it.
Resource-based policies
What if there is no identity to attach the policies to? This is the case for anonymous access, and also when an AWS service does not use a service role, such as an API Gateway. When there is no identity, identity-based policies can not be used.
Many, but not all, AWS services support resource-based policies. These are policies attached to the resource and these can give access when there is no identity.
S3 buckets support bucket policies which can be used to give anonymous access to the bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"<arn>/*"
]
}
]
}
The *
Principal means "everybody" that includes non-users.
Another example is how Lambda allows an API Gateway to call it. This gives the lambda:InvokeFunction
permission to one API:
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "AllowAPIGatewayInvoke",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "<lambda arn>",
"Condition": {
"ArnLike": {
"AWS:SourceArn": "<api gw arn>/sign/*/*"
}
}
}
]
}
Similarly, the role's trust policy is a resource-based policy. This allows services and identities to assume the role. For example, a cross-account access role can use this trust policy to allow access from a different account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<id>:root"
},
"Action": "sts:AssumeRole"
}
]
}
Another example is to allow the Lambda service to use this role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
The Action
in resource-based policies can be one that is relevant to the service. A bucket policy can allow s3:GetObject
, a role the sts:AssumeRole
,
while a Lambda the lambda:InvokeFunction
.
And the Resource,
when needed, can only start with the resource's own ARN. This makes sense as one resource can only give access to itself or
things below it but not to unrelated resources.
How the resource defines who gets access and how specific this control is depends on the service. The Principal
element is always present and that can define
users, roles, services, other accounts, and a few other things.
But, especially with services, this is not granular enough. For example, when a role's trust policy defines the Lambda service, it does not specify which
Lambda function to trust. Similarly, the role for the CloudTrail allows the cloudtrail.amazonaws.com
service, but there is no further restrictions
on which trail.
Some resource policies support conditions to allow only a given resource. For example, the policy for a Lambda function is able to selectively allow the API which can call it:
"Condition": {
"ArnLike": {
"AWS:SourceArn": "<api gw arn>/sign/*/*"
}
}
Unfortunately, there is no generic way to know which resource-based policies support further restrictions on the caller using conditions. You need to check the AWS-provided policy examples to see what conditions are possible.
Using together
When both types of policies are effective then you can use both to give and restrict access. When an IAM user wants to get an object from an S3 bucket then both the policy attached to the user and the one defined on the bucket are effective.
Allowing access
With most services if either of the policies give access to an action then it will be granted. The notable exceptions are KMS key policies and cross-account role trust policies where both the resource and the identity needs an explicit Allow.
As resource-based policies can grant access to identities, the notion that a user can not do anything without attaching policies to it is not true. For example, a bucket policy can allow a user to get objects and that is effective even if the user has no permissions on its own:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<id>:user/user1"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<bucket>/text.txt"
}
]
}
Denying access
On the other hand, policies can also deny access and that trumps everything else. When a deny policy is attached to an identity then it defines what that identity can not do. When such a policy is attached to a resource it specifies who can not access that resource.
Denies in resource policies are powerful security controls. You can define a set of identities and you can be sure that for anyone outside that the access will be denied. For example, this policy denies access to everybody except for a given user:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"NotPrincipal": {
"AWS": "arn:aws:iam::<id>:user/user1"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<bucket>/text.txt"
}
]
}
The NotPrincipal
element matches everybody who is not a given principal and applies a Deny. As long as this policy is in effect, you can be sure
that no matter what permissions other users have they can not access this object. Even administrators are denied.
But, of course, there is a catch here. A user with enough permissions can remove or alter this policy, or add a new access key to the user who is allowed access. That would be a destructive process that leaves an easily-identifiable trace in CloudTrail but it is very hard to prevent. It's a best practice to look at permissions not as a static property of the system but something that can change in ways encoded in itself. That said, a resource policy that denies access is still a powerful construct that makes it easier to think about security.
Conclusion
IAM policies are powerful controls and knowing what types are available is critical for a secure environment. Identity-based policies are more familiar, but resource-based ones usually offer even stricter controls.