Tools: Serving SSE-KMS Encrypted Content from S3 Using CloudFront

Tools: Serving SSE-KMS Encrypted Content from S3 Using CloudFront

Source: Dev.to

Introduction ## What We’re Building (High-Level) ## Why This Setup Matters ## 🔐 Security ## 🔑 Encryption at rest ## ⚡ Performance ## Understanding the Key Services ## What is S3? ## What is CloudFront? ## What is KMS? ## Encryption options in S3 and CloudFront ## How CloudFront Accesses Private S3 (OAI vs OAC) ## The Old Way: OAI (Origin Access Identity) ## The Modern Way: OAC (Origin Access Control) ✅ ## Step-by-Step Setup (AWS Console) ## Step 1: Create Your First KMS Key ## Step 2: Create a Private S3 Bucket ## Step 3: Give CloudFront a Key to Your Bucket (OAC) ## Step 4: Create a CloudFront Distribution ## Step 5: Update S3 Bucket Policy ## Step 6: Update KMS Key Policy for CloudFront ## Step 7: Wait for Deployment ## Step 8: Test Everything ## Common Pitfalls 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. 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: ✅ Encrypted at rest using KMS (SSE-KMS) ✅ Served globally using CloudFront ✅ Bucket never becomes public We’re building a secure content delivery pipeline: This architecture gives you: Before clicking anything, let’s understand what each AWS service is doing in this story. Think of S3 as a massive cloud hard drive. S3 can be public… but for user content, public buckets are dangerous. So we keep it private. 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: KMS (Key Management Service) manages encryption keys. When you use SSE-KMS: This means encryption is: 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: With CloudFront, you can encrypt data in transit using HTTPS, and enforce encryption policy by: So for serious production setups, SSE-KMS is the sweet spot. This is where many people get confused. Your bucket is private. So how does CloudFront fetch the objects? There are 2 approaches: 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. OAC is the newer, recommended approach. 📌 If you’re doing SSE-KMS + CloudFront, use OAC. 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. 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. Now we’ll create the bucket where your files will live. This bucket will be fully private, no accidental public access allowed. 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: 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. 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. CloudFront can now authenticate itself when fetching objects from your private bucket. Now we bring it all together. 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) 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” Go to S3 → your bucket → Permissions → Bucket policy → Edit Paste the copied policy and save. Example bucket policy: Note: AWS:SourceArn will automatically include your distribution’s actual ARN. 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: CloudFront needs time to propagate changes globally (5–15 minutes). 1. Upload a test file (test.jpg) to S3 2. Test CloudFront URL (should succeed): Expected: HTTP/2 200 OK 3. Test direct S3 URL or hit the endpoint on a browser (should fail): Expected: HTTP/1.1 403 Forbidden ✅ Perfect! CloudFront is the only public access point. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: { "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" } } } ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "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" } } } ] } CODE_BLOCK: { "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" } } } ] } CODE_BLOCK: { "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" } } } ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "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" } } } ] } CODE_BLOCK: { "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" } } } ] } COMMAND_BLOCK: curl -I https://d1234abcdef.cloudfront.net/test.jpg Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: curl -I https://d1234abcdef.cloudfront.net/test.jpg COMMAND_BLOCK: curl -I https://d1234abcdef.cloudfront.net/test.jpg COMMAND_BLOCK: curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg COMMAND_BLOCK: curl -I https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg - profile pictures - private attachments - bucket stays private forever - no public ACLs - no “anyone with the link can access it” - objects are encrypted using your KMS key - full audit trail of decrypt operations (CloudTrail) - better compliance posture - CloudFront caches content at edge locations - faster downloads worldwide - reduces S3 request costs - Bucket = container (like a folder) - Object = file (image, pdf, zip, etc.) - 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 - S3 stores objects encrypted - when someone requests the object, S3 decrypts it (with KMS) before returning it - controlled by IAM + key policies - 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 - Redirecting HTTP to HTTPS - Choosing minimal TLS version and ciphers - Selecting a domain name and its associated TLS certificate - SigV4 signing - IAM-style access controls - better security model - Open the AWS Console and search for KMS. - Click Customer managed keys, then Create key. - Choose: Key type: Symmetric Key usage: Encrypt and decrypt - Key type: Symmetric - Key usage: Encrypt and decrypt - 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: myapp-dev-s3-key - Description: “SSE-KMS key for S3 content served via CloudFront” - Alias: myapp-dev-s3-key - Description: “SSE-KMS key for S3 content served via CloudFront” - Select administrators (your IAM user or deployment role), and edit the key policy. - Finish creating the key. - 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. - 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 - Open CloudFront → Origin access → Create control setting - Fill in: Name: myapp-dev-oac Origin type: S3 Signing behavior: Always sign requests - Name: myapp-dev-oac - Origin type: S3 - Signing behavior: Always sign requests - Name: myapp-dev-oac - Origin type: S3 - Signing behavior: Always sign requests - Click Create. - 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) - 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) - 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 - Viewer protocol policy: Redirect HTTP → HTTPS - Allowed methods: GET, HEAD, OPTIONS - Cache policy: CachingOptimized - Viewer protocol policy: Redirect HTTP → HTTPS - Allowed methods: GET, HEAD, OPTIONS - Cache policy: CachingOptimized - Click Create. - 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. - Go to KMS → Customer managed keys → your key → Key policy → Edit - Add the following statement: - AWS:SourceArn → your distribution ARN (not the domain) - kms:ViaService → the region of your S3 bucket - Distribution status: Deploying → Enabled - After policy updates, wait 2–3 minutes for propagation - Optional: Create a cache invalidation (/*) to test immediately - Verify SSE-KMS encryption under Properties → Server-side encryption settings - 403 AccessDenied from CloudFront Wrong AWS:SourceArn in KMS key policy Bucket policy doesn’t match OAC KMS key policy missing CloudFront permissions Policy propagation delay - Wrong AWS:SourceArn in KMS key policy - Bucket policy doesn’t match OAC - KMS key policy missing CloudFront permissions - Policy propagation delay - 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 - 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 - Use the bucket endpoint, not the website endpoint - CloudFront caching old errors Invalidate cache: /* - Invalidate cache: /* - Wrong AWS:SourceArn in KMS key policy - Bucket policy doesn’t match OAC - KMS key policy missing CloudFront permissions - Policy propagation delay - Old files won’t be KMS-encrypted - Fix: Re-upload or copy files to the same bucket with SSE-KMS - Use the bucket endpoint, not the website endpoint - Invalidate cache: /*