Securely downloading encrypted S3 objects using CloudFront
Tags: aws, security, s3
Published: 2023-07-20 06:46:11

We can provision a CloudFront distribution before an S3 bucket if we want to serve our static content securely and more efficiently. When compliance or other requirements demand that we should server-side encrypt the objects with KMS keys, we'll need to make some tweaks in the existing permission sets.

## 1. Problem statement

Say that we want to show our funny cat pictures we store in <a href="https://aws.amazon.com/s3/" class="link" target="_blank" rel="noopener noreferrer">S3</a>  to everybody in the world because they are so great.

It's a good practice to create a **<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html" class="link" target="_blank" rel="noopener noreferrer">CloudFront</a> distribution** with the S3 bucket being the origin. One advantage of this architecture is that we can keep our objects private. It means that users will be able to access the objects only via the distribution that comes with additional <a href="https://aws.amazon.com/cloudfront/security/" class="link" target="_blank" rel="noopener noreferrer">security benefits</a>. The other great feature of CloudFront is that we can serve our pictures from locations close to our users. It is because CloudFront can store our static content in more than 450 <a href="https://aws.amazon.com/about-aws/global-infrastructure/regions_az/" class="link" target="_blank" rel="noopener noreferrer">locations</a> around the world.

But what if we must **encrypt** the objects <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html" class="link" target="_blank" rel="noopener noreferrer">in the bucket</a> using a <a href="https://aws.amazon.com/kms/" class="link" target="_blank" rel="noopener noreferrer">KMS</a> key? Chances are that we can't access the objects straight away, and we'll need to make some adjustments in the permission settings.

## 2. Using Origin Access Control

When we set up the S3 origin in CloudFront, we can use **Origin Access Control (OAC)** to prevent direct access to S3. Formerly Origin Access Identity (OAI) served this purpose but it's a legacy feature now. OAI doesn't support downloading and uploading objects encrypted with a KMS-managed key (SSE-KMS).

So the rest of the blog assumes that we have an Origin Access Control created and a permission statement added to the relevant bucket's policy.

## 3. With different SSE types


Let's see how it works with the different types of available server-side encryption types.

### 3.1. SSE-S3

Encryption is automatically enabled in S3, and the good news is that we can't even turn it off. Encryption with S3-managed keys is the **default** setting when we create a new bucket. If we don't change the bucket's or the objects' encryption settings, S3 will automatically encrypt all objects using SSE-S3.

When we try to access objects encrypted this way, it will just work, so we don't have to take any extra steps.

### 3.2. SSE-KMS

It can happen due to some compliance or other requirements that we must use a dedicated KMS key to encrypt objects in the bucket. Depending on the type of the key we have two scenarios.

**AWS-managed KMS key**

AWS will automatically enable the `aws/s3` KMS key when we first use it. If we try to access an object encrypted with `aws/s3`, we'll get the following error:

```sh
AccessDenied. The ciphertext refers to a customer master key that does not
exist, does not exist in this region, or you are not allowed to access.
```

The error message is generic. It's clear that the key exists in the region, so the only possible reason left is the <q>you are not allowed to access</q> part of the error. Let's take a look at the **key policy**:

```json
{
  "Sid": "Allow access through S3 for all principals in the account that
  are authorized to use S3",
  "Effect": "Allow",
  "Principal": {
    "AWS": "*"
  },
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:ReEncrypt*",
    "kms:GenerateDataKey*",
    "kms:DescribeKey"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:ViaService": "s3.eu-central-1.amazonaws.com",
      "kms:CallerAccount": "123456789012"
    }
  }
}
```

It's interesting because according to the statement ID, all principals in the account with access to S3 can use the key. The distribution has access to the objects because we havd added the relevant permissions to the bucket policy. Also, we can successfully download the objects encrypted with SSE-S3 from the same bucket, so the access to S3 should not be an issue. What's going on then?

The problem is that the `Principal` element along with the `kms:CallerAccount` condition key only allows (or denies) **identities** to use the keys. It means that users, roles or groups that have the relevant S3 permissions can access objects encrypted with `aws/s3`. For example, if Alice is an IAM user with S3 read-only permissions, she would have no problems viewing the encrypted cat pictures.

But the CloudFront distribution is **not** an identity. CloudFront as a service can be a **principal** but it's not a user, role or group. We can't attach any permission statements to the distribution.

A solution would be to add the distribution as principal to the key's resource policy (i.e., the key policy). But the `aws/s3` key is managed by AWS, so we can't edit its policy!

**Customer-managed KMS key**

What if we used a customer-managed KMS key?

Luckily, its **key policy** is editable, and we can control who and how can use the key by adding the relevant permissions.

So if we have the cat pictures encrypted with a key we had created (i.e., a customer-managed key), all we have to do is to add the following statement to the key policy:

```json
{
  "Sid": "Allow Origin Access Identity",
  "Effect": "Allow",
  "Principal": {
    "Service": "cloudfront.amazonaws.com"
  },
  "Action": [
    "kms:Decrypt"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/DISTRIBUTION_ID"
    }
  }
}
```

Services like CloudFront can be **principals** and as such we can add them to **resource-based** policies, like the KMS key policy. We only need `kms:Decrypt` permission because we want S3 to decrypt the objects before it serves them via CloudFront.

To avoid getting a large KMS bill at the end of the month, we can use the customer-managed key as a bucket key.

### 3.3. DSSE-KMS

This scenario is the same as `3.2` except the bucket key part. DSSE-KMS doesn't currently support bucket keys.

## 4. Summary

CloudFront makes it faster and more secure to get objects from S3. When we encrypt the objects with a KMS key, we must add the distribution to the key policy.

AWS-managed KMS keys' policies are not editable, so we should use a customer-managed key to encrypt the objects if we want to use CloudFront for downloading the objects.

## 5. Further reading

<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html" class="link" target="_blank" rel="noopener noreferrer">Getting started with Amazon S3</a> - How to create a bucket in S3

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-creating-console.html" class="link" target="_blank" rel="noopener noreferrer">Creating a distribution</a> - How to create a CloudFront distribution

<a href="https://docs.aws.amazon.com/whitepapers/latest/secure-content-delivery-amazon-cloudfront/s3-origin-with-cloudfront.html" class="link" target="_blank" rel="noopener noreferrer">S3 origin with CloudFront</a> - Bucket policy for Origin Access Control