IAM policy mishaps: Case 1 - S3
Continuing with the series of posts related to IAM misconfigurations, we are going to delve a bit into its use focused on the AWS S3 service.
To do this, we will look at the different ways we can control the security of the service and how dangerous it is to apply a policy without clearly understanding what it does.
Info
If you want to directly try the examples we are going to present, take a look at our repo .
We have prepared different scenarios in Terraform .
We recommend checking out the first part of the series, IAM policy mishaps: Intro to IAM, if you haven't done so yet.
Now, let's talk about Cloud .
The S3 (Simple Storage Service) service is one of the most well-known and used services of AWS. It allows us to store objects with all types of data, which is why it is used by many applications.
Scenario 1
Objective: Grant permission to our application's role, App_A_role
, so it can read and write files in our invoices bucket, invoices-bucket
.
Since our bucket will contain private information, we want to prevent other users from accessing our information.
We have different ways to allow access to our S3 bucket:
Make it public.(1)- Adding IAM permissions to our application's role.
- Using S3 Bucket Policies.
- Using S3 Access Control Lists.
- Using S3 Access Points.
- Using S3 Access Grants.
- No! Not this!
For obvious reasons, we are going to discard the first option, and since this post is about IAM, we will focus on the second option. Additionally, it is the recommended option for what we need.
The easiest way to build an IAM policy is by using the AWS console itself.
In it, we can select the S3 service and we will find the 158 actions available at the time of writing this.
Most likely (and least recommended) is that we become overwhelmed and end up with a policy like this:
{
"Version": "2012-10-17",
"Statement": {
"Sid": "RWS3Access",
"Effect": "Allow",
"Action": [
"s3:*", // (1)!
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket", // (2)!
"arn:aws:s3:::sh3llcon-invoices-bucket/*", // (3)!
]
}
}
s3:*
allows all available actions (now and in the future) on S3.- Actions are allowed on the bucket resource itself.
- Actions are allowed on all objects within the bucket.
If you try this policy, you will find that your application can read and write to your bucket perfectly fine, but... did you know it can also do, among many other things, all of this?
- Delete the bucket.
- Make it public.
- Disable logs, if they are enabled.
- Disable versioning, if it's enabled.
- Disable data retention protections, if they are enabled.
Our application only needs to read and write files, we don't want it to be able to perform any of these actions, whether by mistake or due to a security flaw in our application.
(You'd be surprised how often similar policies are seen. )
Let's improve the policy a bit.
In IAM actions, you have the possibility to use wildcards, so if we use s3:Put*
, we will authorize all S3 actions that are or start with Put
. Since we only want to allow read List
/Get
and write Put
/Delete
, let's try the following policy:
{
"Version": "2012-10-17",
"Statement": {
"Sid": "RWS3Access",
"Effect": "Allow",
"Action": [
"s3:Delete*", // (1)!
"s3:Get*", // (2)!
"s3:List*", // (3)!
"s3:Put*", // (4)!
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket",
"arn:aws:s3:::sh3llcon-invoices-bucket/*",
]
}
}
s3:Delete*
includess3:DeleteBucket
, among other permissions.s3:Get*
allows actions to obtain information about the bucket, for examples3:GetBucketPolicy
.s3:List*
only includess3:ListBucket
, but AWS could add new permissions in the future.s3:Put*
includess3:PutBucketPolicy
, among other permissions.
With this policy, we have removed some unnecessary permissions for the application, but we can still perform the same actions as mentioned above:
Let's continue refining the policy:
{
"Version": "2012-10-17",
"Statement": {
"Sid": "RWS3Access",
"Effect": "Allow",
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:ListBucket",
"s3:PutObject",
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket",
"arn:aws:s3:::sh3llcon-invoices-bucket/*", // (1)!
]
}
}
- This
*
applies to all objects within the bucket, it would be equivalent to specifying the full path of each file. You could also put the condition at the folder level. If you use shared buckets, be careful when applying these wildcards to the resource.
Remember
Asterisks *
are dangerous, make sure you know what you are allowing before using them in your policies.
Tip
If the application should not delete files, remove s3:DeleteObject
. We don't want to allow accidental deletions.
Scenario 2
Let's complicate our scenario a bit.
Objective: Now the bucket will be in a separate account and we will want to have read permission from two roles, App_A_role
and App_B_role
, located in different accounts.
The applications should only be able to list and read the files available in the bucket.
Based on what we've seen before, we will assign the following policy to both roles:
{
"Version": "2012-10-17",
"Statement": {
"Sid": "ROS3Access",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket",
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket",
"arn:aws:s3:::sh3llcon-invoices-bucket/*",
]
}
}
Since the bucket is located in a separate account, we need to add another authorization mechanism. Because the policies assigned to our IAM roles have no effect outside of our account.
So, we will use resource policies (in this case called bucket policies).
We add the following bucket policy to the bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Sh3llconPolicy",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket/*",
"arn:aws:s3:::sh3llcon-invoices-bucket"
],
"Condition": {
"ForAllValues:StringEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::111111111111:role/App_A_role",
"arn:aws:iam::222222222222:role/App_B_role"
]
}
}
}
]
}
If we try from our apps we will have access, but let's try another thing:
Output
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01">
<Name>sh3llcon-invoices-bucket</Name>
<Prefix></Prefix><Marker></Marker><MaxKeys>1000</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents><key>flag.txt</key>
<LastModified>2024-01-24T09:03:12.000Z</LastModified>
<ETag>"e82274fac36cf343b567493489557f84"</ETag><Size>22</Size>
<Owner><ID>e8c8232133f49c01bd96e5dc81337b571fb3daa0819936e9d3f470bd1ff21f60</ID><DisplayName>cloudsec+shellcon</DisplayName></Owner>
<StorageClass>STANDARD</StorageClass></Contents>
</ListBucketResult>
We can also try this using the AWS CLI, but adding the flag --no-sign-request
to be sure we are not using our authorized role.
Ouch... It looks that our bucket has been exposed, let's fix the policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Sh3llconPolicy",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::sh3llcon-invoices-bucket/*",
"arn:aws:s3:::sh3llcon-invoices-bucket"
],
"Condition": {
"StringEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::111111111111:role/App_A_role",
"arn:aws:iam::222222222222:role/App_B_role"
],
"aws:PrincipalOrgID": "0-XXXXXXXX" // (1)!
}
}
}
]
}
- This is optional, but this way we ensure we only allow access from our AWS Organization.
You should not use ForAllValues
or ForAnyValue
with single-value context keys as it can lead to misconfigurations.
As in the case of ForAllValues
, which as explained in the documentation:
It also returns true if there are no context keys in the request, or if the context key value resolves to a null dataset, such as an empty string.
This means that in our tests, where we are not using any roles (and therefore there is no aws:PrincipalArn
), the condition evaluates to True.
Here is an article by Michael Kirchner that explains it in detail.
Tip
To avoid these problems, we must ensure that if we do not need to access the bucket from outside of AWS, we enable block public access.
Conclusion
Not everything are problems, AWS has been including new measures to help protect our buckets:
- Buckets are private by default.
Block public Access
enabled by default.- ACLs are disabled by default.
- Access Analyzer (a service that detects findings in IAM policies) in the bucket policy editor.
But they have also added new ways to grant access to a bucket, so we will always have to stay up to date to review how to keep our buckets secure.
So in the end, the important thing is to keep your systems and yourself updated
See you in the next episode with SNS.
Saludos, and may the force be with you.