Securing data in S3 is a nightmare for many people.

Data breaches from insecure AWS S3 buckets make the news weekly and it’s not just clickbait.

AWS Simple Storage Service (S3) is the world’s most successful object storage service. It offers a wide set of features for managing objects like files at infinite scale, triggering events that integrate with applications, and making those objects accessible to the right users.

It’s this last point that is really hard.

I think these security problems stem from three things:

  1. S3 was launched in March 2006 as the 2nd AWS service (1)
  2. AWS is fanatical about backwards compatibility
  3. Security is a generally and genuinely difficult problem to solve with good usability

In the past 14 years, AWS’ security model has changed greatly as AWS has grown. For reference, AWS’ Identity and Access Management service (IAM) was launched in June 2012 – 6 years after S3 launched.

The current complexity of S3 security is daunting.

Engineers need to configure AWS IAM policy, S3 bucket policy, S3 bucket ACLs, and public bucket access configurations correctly to protect data. Don’t feel bad that you can’t keep it all in your head – S3’s access evaluation flow wasn’t built for humans.

S3’s security model was built feature by feature over a decade and a half. Some of those features interact in nonintuitive ways. Not all of those features and decisions have aged well, e.g. the Authenticated Users S3 User Group. However, AWS seems to keep them around in order to maintain backwards compatibility.

While S3 offers strong security controls to secure data, the usability of those controls is lacking.

Let’s build understanding of the problem by examining how AWS decides whether an API action is permitted.

API action evaluation logic

AWS’ security policy evaluation logic illustrates the decision making process for allowing or denying an API request:

Figure: AWS security policy evaluation logic

I think this diagram makes the evaluation logic appear more linear than it actually is.

Did you notice there are two paths for accessing a resource that supports resource policies? Look for the green end states.

Both a resource policy attached to a bucket and an IAM policy attached to an IAM user or role may grant access to an S3 bucket. If either the bucket or attached IAM policy Allow access to the bucket, the IAM principal is in.

This policy evaluation logic also doesn’t try (and probably shouldn’t) account for service-specific access control systems such as S3’s Object ACLs. Of course the complexity is still there and engineers have to piece it together.

Once you’re aware of S3’s access control tools, your mental model might look like:

Figure: Analyzing access to S3 buckets

This is difficult to track in your head when you have many IAM policies and S3 buckets in use within an AWS account. Maybe impossible.

The most common form of accidental, over-provisioned S3 access I see is that of an IAM principal’s access to an S3 bucket within an account (see footnote 2 for the ‘public bucket’ problem). This over-provisioned IAM principal access results from two common problems in IAM and Bucket policies:

  1. granting an IAM principal a set of s3 api actions without restricting the resource
  2. continuing with the default bucket policy for a bucket, which allows all of that account’s IAM principals to perform any action on the bucket

Let’s examine how these problems manifest before describing a solution.

Problem 1 – Missing resource conditions in IAM policies

First let’s examine the problem of IAM policies that grant access to api actions without a resource condition.

This simple IAM policy gives the IAM role or user access to list all the buckets and read all S3 data in the account (arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess):

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

An IAM principal with this policy may call any of the s3 get or list actions on all resources (buckets). A policy that only allows those actions to be called on a particular bucket would have a resource condition like:

"Resource": "arn:aws:s3:::myapp-data/*"

The wide-ranging access might be ‘obvious’ when you’re staring at this policy and someone points out the Resource condition permits access to all resources (if you’re an AWS security expert). But as application requirements change, skillsets vary, and people come and go, it’s very easy for principals to acquire unintended access.

One way this happens is through use of AWS’ managed policies. These and similar sets of actions are included in many of AWS’ managed policies and used in developer blog posts. Those policies can’t have resource conditions because AWS doesn’t know your resources and what you want to do with them.

So what’s the solution?

A lot of people will then try to manage access to their most critical data with S3 bucket policies once they recognize the danger of the default S3 behavior.

This leads to the second problem.

Problem 2 – Missing DenyEveryoneElse in Resource Policies

The second problem is creating a bucket policy that Allows the intended set of IAM principals to access the bucket, but does not Deny access to all other principals.

Remember, there are two paths into the bucket IAM and the Allow statements are OR‘d, not AND‘d.

Any Allow in the policy evaluation chain provides access to the resource unless there is an explicit Deny.

To protect against unintentional access grants in IAM, an S3 resource policy needs to:

  • Allow the intended principals
  • Deny everyone else <— the missing bit

Let’s create a ‘least-privilege’ bucket policy.

Example: least-privilege bucket policy

Suppose you have a credit processing application that executes with an IAM role named credit-processor-prod and needs to get and put objects into the credit-applications S3 bucket.

The credit-applications bucket could restrict access to only the credit processing application role with an S3 bucket policy like:

{
  "Version": "2012-10-17",
  "Id": "CreditApplications",
  "Statement": [{
      "Sid": "AllowCreditAppProcessing",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::1234:role/credit-processor-prod"
        ]
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:PutObjectACL
      ],
      "Resource": [
        "arn:aws:s3:::credit-applications",
        "arn:aws:s3:::credit-applications/*",
      ]
    },{
        "Sid": "DenyEveryoneElse",
        "Effect": "Deny",
        "Principal": "*",
        "Action": "s3:*",
        "Resource": [
          "arn:aws:s3:::credit-applications",
          "arn:aws:s3:::credit-applications/*",
        ],
        "Condition": {
          "ArnNotEquals": {
            "aws:PrincipalArn": [
              "arn:aws:iam::1234:role/credit-processor-prod"
            ]
        }
      }
    }
  ]
}

The DenyEveryoneElse statement is the bit that many buckets containing critical data are missing. The DenyEveryoneElse statement denies all S3 API actions to the bucket unless the request is made by the credit-processor-prod principal. This explicit Deny takes precedence over Allows made in IAM policies.

Next Steps

Implementing least-privilege access in AWS requires careful engineering of resource conditions in IAM and resource policies. This should give you a starting point for how to do that. Once you have access control ironed out, you may want to use bucket policies to enforce other security practices. You might require encryption during transport, encryption at rest, and use of certain encryption keys.

If you’d like help keeping your S3 bucket out of the news, check out the k9 S3 bucket security tune-up. We’ll work together 1 on 1 to analyze and improve your bucket’s security policies so your data is only accessible by the people and applications you intend.

Stephen

#NoDrama

(1) Simple Queue Service (SQS) was the first public AWS service in Nov 2004; S3 and EC2 were launched in 2006

(2) Less frequent, but generally worse problem: Accidentally making a bucket and its objects publicly accessible via default object ACLs