Serving SSE-KMS Encrypted Content from S3 Using CloudFront
Source: Dev.to
The problem
You’re building an application where users upload:
- profile pictures
- invoices
- receipts
- documents
- private attachments
You need a solution that satisfies both:
- Privacy – files must stay private.
- Performance – files must load fast worldwide.
| Option | Pros | Cons |
|---|---|---|
| Public S3 bucket | Fast (CDN‑friendly) | Not secure |
| Private S3 bucket | Secure | Potentially slower, not CDN‑friendly |
| Private + own‑key encryption | Secure even if storage is compromised | Needs extra setup |
Enter SSE‑KMS.
The ideal setup becomes:
- ✅ Private S3 bucket
- ✅ Encrypted at rest using KMS (SSE‑KMS)
- ✅ Served globally via CloudFront
- ✅ Bucket never becomes public
Architecture overview
User ──► CloudFront (edge cache) ──► S3 (private, SSE‑KMS)
What each service does
-
S3 – massive cloud “hard drive”.
- Bucket = container (folder)
- Object = file (image, PDF, ZIP, …)
- Can be public, but we keep it private for user content.
-
CloudFront – AWS CDN with edge locations worldwide.
- When a request arrives (e.g.,
https://d1234abcdef.cloudfront.net/images/photo.jpg):- Checks edge cache.
- If cached → returns instantly.
- If not cached → fetches from S3, caches, then returns.
- When a request arrives (e.g.,
-
KMS (Key Management Service) – manages encryption keys.
- With SSE‑KMS, S3 stores objects encrypted.
- On request, S3 decrypts using the KMS key before returning.
- Benefits: automatic encryption, IAM‑controlled access, full audit trail (CloudTrail).
Server‑Side Encryption (SSE) flavors
| Type | Description |
|---|---|
| SSE‑S3 | S3‑managed keys; each object gets a unique key. |
| SSE‑KMS | Customer Master Keys (CMKs) stored in AWS KMS – more control & visibility. |
| SSE‑C | Customer‑provided keys; you manage the keys, S3 only encrypts objects. |
Encrypting data in transit (CloudFront)
- Enforce HTTPS (redirect HTTP → HTTPS).
- Choose minimal TLS version & cipher suites.
- Attach a custom domain with an associated TLS certificate.
Bottom line: For production, SSE‑KMS + CloudFront is the sweet spot.
How CloudFront accesses a private, SSE‑KMS‑protected bucket
Two approaches exist:
-
Origin Access Identity (OAI) – a special CloudFront user with S3 permissions.
- Limitation: OAI only supports SSE‑S3, not SSE‑KMS.
-
Origin Access Control (OAC) – the newer, recommended method.
- Uses SigV4 signing, IAM‑style access controls, and a stronger security model.
📌 If you’re using SSE‑KMS + CloudFront, use OAC.
What you’ll build
By the end of this guide you’ll have:
- A private S3 bucket with objects encrypted using SSE‑KMS.
- A CloudFront distribution that securely serves those objects worldwide.
Step‑by‑step guide
Step 1 – Create your first KMS key
- Open the AWS Console → KMS.
- Click Customer managed keys → Create key.
- Key type: Symmetric
Key usage: Encrypt and decrypt - Alias:
myapp-dev-s3-key - Description: “SSE‑KMS key for S3 content served via CloudFront”
- Assign administrators (your IAM user/role) and edit the key policy as needed.
- Finish creating the key.
This key becomes the “master key” for all S3 objects you’ll store.
Step 2 – Create a private S3 bucket
- Go to S3 → Create bucket.
- Bucket name: e.g.,
myapp-dev-private-assets-123456(must be globally unique). - Choose the same region as your KMS key.
- Block all public access: Turn on to prevent accidental exposure.
- (Optional) Enable versioning – protects against accidental deletion.
- Default encryption:
- Select SSE‑KMS.
- Paste the ARN of the KMS key you created (
myapp-dev-s3-key). - Enable Bucket Key to reduce KMS request costs.
Note: Bucket‑level encryption only applies to objects uploaded after it’s enabled.
Step 3 – Give CloudFront a key to your bucket (OAC)
- Open CloudFront → Origin access → Create origin access control.
- Name:
myapp-dev-oac(or any descriptive name). - Signing behavior: SigV4.
- Origin type: S3.
- Access: Read‑only (or as required).
- Save the OAC.
Step 4 – Create the CloudFront distribution
- In CloudFront, click Create Distribution → Web.
- Origin Settings:
- Origin domain: Select the private S3 bucket you created.
- Origin Access Control: Choose the OAC you just made.
- Origin request policy: Use the default or create one that forwards the necessary headers.
- Cache Behavior Settings:
- Viewer Protocol Policy: Redirect HTTP to HTTPS.
- Allowed HTTP Methods:
GET, HEAD, OPTIONS(add others if needed). - Cache based on selected request headers: Whitelist only the headers required for your app.
- Distribution Settings:
- Price class: Choose based on your target regions.
- Alternate domain names (CNAMEs): Add your custom domain if you have one.
- SSL certificate: Choose Custom SSL (ACM‑provided) for your domain.
- Click Create Distribution and wait for deployment (usually a few minutes).
Step 5 – Update the bucket policy for the OAC
Replace the placeholder with the OAC’s IAM principal ARN (visible in the OAC console) and adjust the bucket name accordingly.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <OAC-ARN>"
},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::myapp-dev-private-assets-123456/*"
}
]
}
Save the policy.
Step 6 – Test the end‑to‑end flow
-
Upload a test file to the S3 bucket (e.g.,
test.txt). -
Verify the object’s Encryption column shows SSE‑KMS and the correct KMS key.
-
Access the file via the CloudFront domain:
curl -I https://d1234abcdef.cloudfront.net/test.txt- The request should succeed (HTTPS).
- CloudFront will cache the object at the edge, then serve it to subsequent users instantly.
-
Check CloudTrail for decrypt events – you should see KMS usage logs for each request.
Benefits recap
- Bucket stays private forever – no public ACLs or “anyone with the link” access.
- Objects encrypted with your KMS key – full control and auditability.
- Full decrypt audit trail via CloudTrail → better compliance.
- CloudFront edge caching → faster downloads worldwide and reduced S3 request costs.
TL;DR
- Private S3 bucket + SSE‑KMS = data‑at‑rest encryption you control.
- CloudFront OAC (SigV4) = secure, CDN‑friendly access to that bucket.
- Result: Secure, fast, globally‑distributed content delivery without ever exposing your bucket publicly.
Step‑by‑Step Guide: Serve SSE‑KMS‑Encrypted S3 Objects via CloudFront Using an OAC
Step 1 – Create an Origin Access Control (OAC)
- Navigate:
CloudFront → Origin Access Controls → Create OAC - Fill in:
| Field | Value |
|---|---|
| Name | myapp-dev-oac |
| Origin type | S3 |
| Signing behavior | Always sign requests |
- Click Create.
CloudFront can now authenticate itself when fetching objects from your private bucket.
Step 2 – Create a CloudFront Distribution
-
Navigate:
CloudFront → Distributions → Create distribution -
Configure the origin:
-
Origin domain: Select your S3 bucket
-
⚠️ Critical: Use the bucket endpoint, e.g.
myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.comDo not use the website endpoint (e.g.
s3-website.eu-west-2.amazonaws.com). -
Origin access: Choose the OAC you just created (
myapp‑dev-oac).
-
-
Configure cache behavior:
| Setting | Value |
|---|---|
| Viewer protocol policy | Redirect HTTP → HTTPS |
| Allowed methods | GET, HEAD, OPTIONS |
| Cache policy | CachingOptimized |
- Click Create.
Step 3 – Update the S3 Bucket Policy
- On the distribution’s page you’ll see a 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 (replace placeholders with your values):
{
"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:SourceArnwill automatically include your distribution’s actual ARN after you save.
Step 4 – Update the KMS Key Policy for CloudFront
Because your objects are SSE‑KMS encrypted, CloudFront needs permission to decrypt them.
- Navigate to
KMS → Customer managed keys → <your key> → Key policy → Edit. - Add the following statement (replace placeholders):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnableIAMUserPermissions",
"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::<account-id>:distribution/<distribution-id>",
"kms:ViaService": "s3.eu-west-2.amazonaws.com"
}
}
}
]
}
- Replace
AWS:SourceArn→ your distribution ARN.kms:ViaService→ the region of your S3 bucket (e.g.,s3.eu-west-2.amazonaws.com).
Step 5 – Wait for Deployment
- CloudFront propagation: 5–15 minutes.
- Distribution status changes:
Deploying → Enabled. - After policy updates, allow 2–3 minutes for propagation.
Optional: Create a cache invalidation (/*) to test immediately.
Step 6 – Test Everything
-
Upload a test file (e.g.,
test.jpg) to the S3 bucket. Verify SSE‑KMS encryption under Properties → Server‑side encryption. -
Test via CloudFront (should succeed):
curl -I https://d1234abcdef.cloudfront.net/test.jpgExpected:
HTTP/2 200 OK -
Test direct S3 URL (should fail):
curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpgExpected:
HTTP/1.1 403 Forbidden
✅ Result: CloudFront is the only public access point.
Common Pitfalls & Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| 403 AccessDenied from CloudFront | Wrong AWS:SourceArn in KMS key policy, bucket policy mismatch, missing CloudFront permissions, propagation delay | Verify ARN values, ensure OAC is referenced, wait for propagation |
| Files not encrypted | Files uploaded before SSE‑KMS was enabled | Re‑upload or copy the objects with SSE‑KMS enabled |
| Wrong S3 origin endpoint | Using website endpoint instead of bucket endpoint | Use the bucket endpoint (*.s3.<region>.amazonaws.com) |
| CloudFront keeps serving old errors | Cached error responses | Invalidate the cache (/*) |
You now have a secure, CloudFront‑fronted S3 bucket with SSE‑KMS‑encrypted objects, accessible only through the CloudFront distribution.