Serving SSE-KMS Encrypted Content from S3 Using CloudFront

Published: (January 30, 2026 at 07:18 PM EST)
6 min read
Source: Dev.to

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:

  1. Privacy – files must stay private.
  2. Performance – files must load fast worldwide.
OptionProsCons
Public S3 bucketFast (CDN‑friendly)Not secure
Private S3 bucketSecurePotentially slower, not CDN‑friendly
Private + own‑key encryptionSecure even if storage is compromisedNeeds 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):
      1. Checks edge cache.
      2. If cached → returns instantly.
      3. If not cached → fetches from S3, caches, then returns.
  • 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

TypeDescription
SSE‑S3S3‑managed keys; each object gets a unique key.
SSE‑KMSCustomer Master Keys (CMKs) stored in AWS KMS – more control & visibility.
SSE‑CCustomer‑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:

  1. Origin Access Identity (OAI) – a special CloudFront user with S3 permissions.

    • Limitation: OAI only supports SSE‑S3, not SSE‑KMS.
  2. 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

  1. Open the AWS Console → KMS.
  2. Click Customer managed keysCreate key.
  3. Key type: Symmetric
    Key usage: Encrypt and decrypt
  4. Alias: myapp-dev-s3-key
  5. Description: “SSE‑KMS key for S3 content served via CloudFront”
  6. Assign administrators (your IAM user/role) and edit the key policy as needed.
  7. Finish creating the key.

This key becomes the “master key” for all S3 objects you’ll store.

Step 2 – Create a private S3 bucket

  1. Go to S3Create bucket.
  2. Bucket name: e.g., myapp-dev-private-assets-123456 (must be globally unique).
  3. Choose the same region as your KMS key.
  4. Block all public access: Turn on to prevent accidental exposure.
  5. (Optional) Enable versioning – protects against accidental deletion.
  6. 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)

  1. Open CloudFrontOrigin accessCreate origin access control.
  2. Name: myapp-dev-oac (or any descriptive name).
  3. Signing behavior: SigV4.
  4. Origin type: S3.
  5. Access: Read‑only (or as required).
  6. Save the OAC.

Step 4 – Create the CloudFront distribution

  1. In CloudFront, click Create DistributionWeb.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Upload a test file to the S3 bucket (e.g., test.txt).

  2. Verify the object’s Encryption column shows SSE‑KMS and the correct KMS key.

  3. 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.
  4. 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)

  1. Navigate: CloudFront → Origin Access Controls → Create OAC
  2. Fill in:
FieldValue
Namemyapp-dev-oac
Origin typeS3
Signing behaviorAlways sign requests
  1. Click Create.
    CloudFront can now authenticate itself when fetching objects from your private bucket.

Step 2 – Create a CloudFront Distribution

  1. Navigate: CloudFront → Distributions → Create distribution

  2. 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.com

      Do 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).

  3. Configure cache behavior:

SettingValue
Viewer protocol policyRedirect HTTP → HTTPS
Allowed methodsGET, HEAD, OPTIONS
Cache policyCachingOptimized
  1. Click Create.

Step 3 – Update the S3 Bucket Policy

  1. On the distribution’s page you’ll see a blue banner: “The S3 bucket policy needs to be updated.”
  2. Click Copy policy.
  3. Go to S3 → <your bucket> → Permissions → Bucket policy → Edit.
  4. 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:SourceArn will 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.

  1. Navigate to KMS → Customer managed keys → <your key> → Key policy → Edit.
  2. 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

  1. Upload a test file (e.g., test.jpg) to the S3 bucket. Verify SSE‑KMS encryption under Properties → Server‑side encryption.

  2. Test via CloudFront (should succeed):

    curl -I https://d1234abcdef.cloudfront.net/test.jpg

    Expected: HTTP/2 200 OK

  3. Test direct S3 URL (should fail):

    curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg

    Expected: HTTP/1.1 403 Forbidden

Result: CloudFront is the only public access point.

Common Pitfalls & Fixes

SymptomLikely CauseFix
403 AccessDenied from CloudFrontWrong AWS:SourceArn in KMS key policy, bucket policy mismatch, missing CloudFront permissions, propagation delayVerify ARN values, ensure OAC is referenced, wait for propagation
Files not encryptedFiles uploaded before SSE‑KMS was enabledRe‑upload or copy the objects with SSE‑KMS enabled
Wrong S3 origin endpointUsing website endpoint instead of bucket endpointUse the bucket endpoint (*.s3.<region>.amazonaws.com)
CloudFront keeps serving old errorsCached error responsesInvalidate the cache (/*)

You now have a secure, CloudFront‑fronted S3 bucket with SSE‑KMS‑encrypted objects, accessible only through the CloudFront distribution.

Back to Blog

Related posts

Read more »