TL;DR
To correctly use Deny effect together with NotPrincipal, instead of specifying the NotPrincipal, add a StringNotEquals condition for AWS:PrincipalARN
. Skip to example.
The wrong way to use "effect": "Deny"
with "NotPrincipal"
When it comes to granting permissions, it's as easy as allowing the required permissions for a specific role or user. However, it's not so easy when you're working in a shared account where others have the same access to your resources as you do.
Don't you hate when other users mess with your stuff or even worse - they accidentally expose their credentials and now the whole world has access to your S3 bucket... To avoid worrying, you thought it was a great idea to add a Deny policy to your resource, denying access to everyone but yourself.
The following is an example of how not to do it. Try to spot why. Scroll down for the correct way.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"NotPrincipal": {
"AWS": "arn:aws:iam::000000000000:role/<my-user-role>"
},
"Action": "s3:*",
"Resource": "arn:aws:s3:::<my-bucket>/*"
}
]
}
Good job, you've locked yourself out of your bucket! Nor you, the root account user nor anyone else will be able to do anything with the bucket, it is essentially rendered useless. While the above policy might look valid, the problem is that when a user/role performs an action, it's principal consists of the following:
"arn:aws:sts::000000000000:assumed-role/<my-user-role>/<instance-id>"
"arn:aws:iam::000000000000:role/<my-user-role>"
"arn:aws:iam::000000000000:root"
So when you set the Deny to ignore your role, it implicitly denied your assumed-role
and root
resulting in denying you access to your resource.
Could I not just add my assumed role and root to the exception list? You could... But for example, each Lambda function has a different assumed-role and you can't use wildcard characters in Principal values. I don't know about you, but when Amazon's official documentation on NotPrincipal recommends not to use it, I'd rather find a better way.
The correct ways
The slightly harder way
That involves calling aws sts get-caller-identity
for every user, populating policies with ids that make no sense and doesn't really work with functional roles (unless you plan to invoke sts get-caller-identity from inside your lambda/ec2/etc)
I'll just refer you to AWS blog posts here and here.
The easy way (Recommended)
According to the AWS Global Condition Key documentation, there is a key called aws:PrincipalArn
Which is great, because:
- It is always included in the request content;
- It returns the ARN of the role instead of the assumed-role;
- It supports wildcards;
- Global Condition Keys are available for every action.
There is a mistake in the documentation, as aws:PrincipalArn does not work with ARN operators, instead, it works with String operators.
All of this means, that instead of using "NotPrincipal", we can specify a "StringNotEquals" condition on "aws:PrincipalArn" and specifying our roles ARN. Don't worry if you've never used a Condition before, pick one of the following examples and adapt it to your needs.
Allowing access only to a single ARN
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<my-bucket>/*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::000000000000:role/<my-user-role>",
}
}
}
]
}
Let's say we have different roles named:
- my-project-lambda
- my-project-ec2
- my-project-console
We can use a placeholder (*) to select them all at the same time by using "StringNotLike" instead of "StringNotEquals"
Using placeholders
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<my-bucket>/*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": "arn:aws:iam::000000000000:role/my-project-*",
}
}
}
]
}
Wanted just to benefit from "aws:PrincipalArn" without having to deny anything? Use it with allow in the same way!
Allowing with multiple roles
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<my-bucket>/*",
"Condition": {
"StringEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::000000000000:role/<my-user-role>",
"arn:aws:iam::000000000000:role/<my-manager-role>",
"arn:aws:iam::000000000000:role/<my-audit-role>"
]
}
}
}
]
}
This is most powerful when working in a corporate AWS account. Especially when there is a dedicated team managing Identity-based policies and roles, but developers themselves manage Resource-based policies. Sadly not all resources can have their own policies, a full list of those that can is available here.
That is all I wanted to share today, best of luck in your Cloud endeavors and let me know in the comments of all the resources you've locked yourself out of.
Top comments (3)
"There is a mistake in the documentation, as aws:PrincipalArn does not work with ARN operators, instead, it works with String operators."
Golden information!
Thanks, great advice...
Great article. I used this approach for AWS Secrets Manager protecting my secret from developers in our account with wide access.