Introduction
A best practice for your web applications is to use Amazon S3 to store content and Amazon CloudFront to deliver it to users and protecting your data at rest and in transit. Encryption is one of protection controls AWS provides you to reduce the risks of unauthorized access, loss, or exposure. In this blog post, you will learn how to implement one of these options (SSE-KMS) in S3 when using CloudFront for content delivery.
Let’s say you’re building an application.
Users upload:
- profile pictures
- invoices
- receipts
- documents
- private attachments
Now you’re stuck with a very real engineering dilemma:
“I want the files to be private, but I also want them to load fast anywhere in the world.”
If you store the files in a public S3 bucket → fast, but not secure.
If you store the files in a private S3 bucket → secure, but sometimes slower, and not CDN-friendly.
And then you go one step further:
“I also want encryption at rest using my own key… so even if someone gets access to the storage layer, they still can’t read anything.”
That’s where SSE-KMS comes in.
So the perfect setup becomes:
✅ Private S3 bucket
✅ Encrypted at rest using KMS (SSE-KMS)
✅ Served globally using CloudFront
✅ Bucket never becomes public
What We’re Building (High-Level)
We’re building a secure content delivery pipeline:
Why This Setup Matters
This architecture gives you:
🔐 Security
- bucket stays private forever
- no public ACLs
- no “anyone with the link can access it”
🔑 Encryption at rest
- objects are encrypted using your KMS key
- full audit trail of decrypt operations (CloudTrail)
- better compliance posture
⚡ Performance
- CloudFront caches content at edge locations
- faster downloads worldwide
- reduces S3 request costs
Understanding the Key Services
Before clicking anything, let’s understand what each AWS service is doing in this story.
What is S3?
Think of S3 as a massive cloud hard drive.
- Bucket = container (like a folder)
- Object = file (image, pdf, zip, etc.)
S3 can be public… but for user content, public buckets are dangerous.
So we keep it private.
What is CloudFront?
CloudFront is AWS’s CDN.
It has servers around the world called edge locations.
When a user requests:
https://d1234abcdef.cloudfront.net/images/photo.jpg
CloudFront does this:
- checks if it already cached the file at the nearest edge
- if cached → returns immediately (super fast)
- if not cached → fetches from origin (S3), caches it, then returns it
What is KMS?
KMS (Key Management Service) manages encryption keys.
When you use SSE-KMS:
- S3 stores objects encrypted
- when someone requests the object, S3 decrypts it (with KMS) before returning it
This means encryption is:
- automatic
- controlled by IAM + key policies
- auditable
Encryption options in S3 and CloudFront
With S3, you can either encrypt data at the client side and then upload the encrypted data to your S3 bucket, or to let S3 encrypt your data before storing it. The second method is called server-side encryption (SSE), and it comes in multiple flavors:
- Server-Side Encryption with Amazon S3-Managed Keys (SSE-S3), where each object is encrypted with a unique key managed by S3
- Server-Side Encryption with Customer Master Keys (CMKs) stored in AWS Key Management Service (SSE-KMS). This gives you more control and visibility into how your encryption keys are being used
- Server-Side Encryption with customer-provided keys (SSE-C), where you manage the encryption keys and S3 only manages the encryption of objects
With CloudFront, you can encrypt data in transit using HTTPS, and enforce encryption policy by:
- Redirecting HTTP to HTTPS
- Choosing minimal TLS version and ciphers
- Selecting a domain name and its associated TLS certificate
So for serious production setups, SSE-KMS is the sweet spot.
How CloudFront Accesses Private S3 (OAI vs OAC)
This is where many people get confused.
Your bucket is private.
So how does CloudFront fetch the objects?
There are 2 approaches:
The Old Way: OAI (Origin Access Identity)
OAI is a special CloudFront user that is associated with an S3 origin and given the necessary permissions to access to objects within the bucket. Currently, OAI only supports SSE-S3, which means customers cannot use SSE-KMS with OAI. It worked fine for SSE-S3, but it does not play well with SSE-KMS.
AWS has basically moved on from this.
The Modern Way: OAC (Origin Access Control) ✅
OAC is the newer, recommended approach.
It uses:
- SigV4 signing
- IAM-style access controls
- better security model
📌 If you’re doing SSE-KMS + CloudFront, use OAC.
Step-by-Step Setup (AWS Console)
By the end of this guide, you’ll have a private S3 bucket with objects encrypted using SSE-KMS, served securely through a CloudFront distribution.
Step 1: Create Your First KMS Key
Think of this as the “master key” for your S3 files. Every object we store will be encrypted with it, and CloudFront will need this key to decrypt content for your users.
Open the AWS Console and search for KMS.
Click Customer managed keys, then Create key.
- Choose:
- Key type: Symmetric
- Key usage: Encrypt and decrypt
- Give your key a friendly label:
-
Alias:
myapp-dev-s3-key - Description: “SSE-KMS key for S3 content served via CloudFront”
-
Alias:
- Select administrators (your IAM user or deployment role), and edit the key policy.
- Finish creating the key.
Step 2: Create a Private S3 Bucket
Now we’ll create the bucket where your files will live. This bucket will be fully private, no accidental public access allowed.
- Go to S3 → Create bucket.
-
Give it a unique name, e.g.,
myapp-dev-private-assets-123456.(Bucket names must be globally unique.)
Choose the same region as your KMS key.
Block all public access:
Turn on Block all public access to prevent accidental exposure.
Enable versioning (optional but recommended):
Turn on versioning to protect your files from accidental deletion.
Enable SSE-KMS encryption:
Under Default encryption:
- Select SSE-KMS
- Input the arn of the KMS key you created (
myapp-dev-s3-key) - Enable Bucket Key to reduce costs for large buckets

Now your private, encrypted bucket is ready.
⚠️ Note: Bucket encryption only applies to files uploaded after it’s enabled. Files uploaded beforehand won’t be KMS-encrypted.
Step 3: Give CloudFront a Key to Your Bucket (OAC)
CloudFront needs a way to prove it’s allowed to fetch your private S3 objects. That’s what Origin Access Control (OAC) does — think of it as giving CloudFront a secure ID card.
- Open CloudFront → Origin access → Create control setting
- Fill in:
- Name:
myapp-dev-oac - Origin type: S3
- Signing behavior: Always sign requests
- Name:
- Click Create.
CloudFront can now authenticate itself when fetching objects from your private bucket.
Step 4: Create a CloudFront Distribution
Now we bring it all together.
- Go to CloudFront → Distributions → Create distribution
-
Configure the origin:
-
Origin domain: select your S3 bucket
⚠️ Critical: Choose the bucket endpoint, e.g., myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com
Do not use the website endpoint (e.g.,
s3-website.eu-west-2.amazonaws.com) Origin access: Use the OAC you just created (
myapp-dev)
-
- Configure cache behavior:
- Viewer protocol policy: Redirect HTTP → HTTPS
- Allowed methods: GET, HEAD, OPTIONS
- Cache policy: CachingOptimized
- Click Create.
Step 5: Update S3 Bucket Policy
This step is crucial. You must update your bucket policy so CloudFront can access objects using the OAC.
On the CloudFront distribution page, look for the blue banner: “The S3 bucket policy needs to be updated”
Click Copy policy
Go to S3 → your bucket → Permissions → Bucket policy → Edit
Paste the copied policy and save.
Example bucket policy:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::myapp-dev-private-assets-123456/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456785012:distribution/E2ABCDEFG12345"
}
}
}
]
}
Note: AWS:SourceArn will automatically include your distribution’s actual ARN.
Step 6: Update KMS Key Policy for CloudFront
Since your files are SSE-KMS encrypted, CloudFront needs permission to decrypt them. If it doesn’t, you’ll get 403 AccessDenied errors.
Go to KMS → Customer managed keys → your key → Key policy → Edit
Add the following statement:
{
"Version": "2012-10-17",
"Id": "key-consolepolicy-3",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<account_id>:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowCloudFrontDecryptThroughS3Only",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::921950937336:distribution/d303chch2n9rqs",
"kms:ViaService": "s3.eu-north-1.amazonaws.com"
}
}
}
]
}
⚠️ Replace:
AWS:SourceArn→ your distribution ARN (not the domain)kms:ViaService→ the region of your S3 bucket
Step 7: Wait for Deployment
CloudFront needs time to propagate changes globally (5–15 minutes).
- Distribution status: Deploying → Enabled
- After policy updates, wait 2–3 minutes for propagation
- Optional: Create a cache invalidation (
/*) to test immediately
Step 8: Test Everything
1. Upload a test file (test.jpg) to S3
- Verify SSE-KMS encryption under Properties → Server-side encryption settings
2. Test CloudFront URL (should succeed):
curl -I https://d1234abcdef.cloudfront.net/test.jpg
Expected: HTTP/2 200 OK
3. Test direct S3 URL or hit the endpoint on a browser (should fail):
curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg
Expected: HTTP/1.1 403 Forbidden
✅ Perfect! CloudFront is the only public access point.
Common Pitfalls
-
403 AccessDenied from CloudFront
- Wrong
AWS:SourceArnin KMS key policy - Bucket policy doesn’t match OAC
- KMS key policy missing CloudFront permissions
- Policy propagation delay
- Wrong
-
Files uploaded before encryption enabled
- Old files won’t be KMS-encrypted
- Fix: Re-upload or copy files to the same bucket with SSE-KMS
-
Wrong S3 Origin Endpoint
- Use the bucket endpoint, not the website endpoint
-
CloudFront caching old errors
- Invalidate cache:
/*
- Invalidate cache:














Top comments (0)