<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdulrasheed Abdulazeez</title>
    <description>The latest articles on DEV Community by Abdulrasheed Abdulazeez (@abdulrasheed_abdulazeez_7).</description>
    <link>https://dev.to/abdulrasheed_abdulazeez_7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1753574%2Fa47a09e2-bd58-48a8-90af-46791bb94af7.jpg</url>
      <title>DEV Community: Abdulrasheed Abdulazeez</title>
      <link>https://dev.to/abdulrasheed_abdulazeez_7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdulrasheed_abdulazeez_7"/>
    <language>en</language>
    <item>
      <title>Serving SSE-KMS Encrypted Content from S3 Using CloudFront</title>
      <dc:creator>Abdulrasheed Abdulazeez</dc:creator>
      <pubDate>Sat, 31 Jan 2026 00:18:26 +0000</pubDate>
      <link>https://dev.to/abdulrasheed_abdulazeez_7/serving-sse-kms-encrypted-content-from-s3-using-cloudfront-5c2b</link>
      <guid>https://dev.to/abdulrasheed_abdulazeez_7/serving-sse-kms-encrypted-content-from-s3-using-cloudfront-5c2b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ukav9metoxopg9qknv8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ukav9metoxopg9qknv8.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A best practice for your web applications is to use &lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; to store content and &lt;a href="https://aws.amazon.com/cloudfront/" rel="noopener noreferrer"&gt;Amazon CloudFront&lt;/a&gt; to deliver it to users and protecting your data at &lt;a href="https://wa.aws.amazon.com/wat.question.SEC_9.en.html" rel="noopener noreferrer"&gt;rest&lt;/a&gt; and in &lt;a href="https://wa.aws.amazon.com/wat.question.SEC_10.en.html" rel="noopener noreferrer"&gt;transit&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;Let’s say you’re building an application.&lt;/p&gt;

&lt;p&gt;Users upload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;profile pictures&lt;/li&gt;
&lt;li&gt;invoices&lt;/li&gt;
&lt;li&gt;receipts&lt;/li&gt;
&lt;li&gt;documents&lt;/li&gt;
&lt;li&gt;private attachments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you’re stuck with a very real engineering dilemma:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I want the files to be private, but I also want them to load fast anywhere in the world.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you store the files in a public S3 bucket → fast, but &lt;strong&gt;not secure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you store the files in a private S3 bucket → secure, but sometimes slower, and not CDN-friendly.&lt;/p&gt;

&lt;p&gt;And then you go one step further:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“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.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s where &lt;strong&gt;SSE-KMS&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;So the perfect setup becomes:&lt;/p&gt;

&lt;p&gt;✅ Private S3 bucket&lt;/p&gt;

&lt;p&gt;✅ Encrypted at rest using KMS (SSE-KMS)&lt;/p&gt;

&lt;p&gt;✅ Served globally using CloudFront&lt;/p&gt;

&lt;p&gt;✅ Bucket never becomes public&lt;/p&gt;

&lt;h2&gt;
  
  
  What We’re Building (High-Level)
&lt;/h2&gt;

&lt;p&gt;We’re building a secure content delivery pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxvim0ryilmefn3i2tym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxvim0ryilmefn3i2tym.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Setup Matters
&lt;/h2&gt;

&lt;p&gt;This architecture gives you:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔐 Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;bucket stays private forever&lt;/li&gt;
&lt;li&gt;no public ACLs&lt;/li&gt;
&lt;li&gt;no “anyone with the link can access it”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔑 Encryption at rest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;objects are encrypted using &lt;strong&gt;your KMS key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;full audit trail of decrypt operations (CloudTrail)&lt;/li&gt;
&lt;li&gt;better compliance posture&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚡ Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront caches content at edge locations&lt;/li&gt;
&lt;li&gt;faster downloads worldwide&lt;/li&gt;
&lt;li&gt;reduces S3 request costs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Key Services
&lt;/h2&gt;

&lt;p&gt;Before clicking anything, let’s understand what each AWS service is doing in this story.&lt;/p&gt;




&lt;h3&gt;
  
  
  What is S3?
&lt;/h3&gt;

&lt;p&gt;Think of S3 as a massive cloud hard drive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket&lt;/strong&gt; = container (like a folder)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object&lt;/strong&gt; = file (image, pdf, zip, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 can be public… but for user content, public buckets are dangerous.&lt;/p&gt;

&lt;p&gt;So we keep it private.&lt;/p&gt;




&lt;h3&gt;
  
  
  What is CloudFront?
&lt;/h3&gt;

&lt;p&gt;CloudFront is AWS’s CDN.&lt;/p&gt;

&lt;p&gt;It has servers around the world called &lt;strong&gt;edge locations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a user requests:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://d1234abcdef.cloudfront.net/images/photo.jpg&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;CloudFront does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;checks if it already cached the file at the nearest edge&lt;/li&gt;
&lt;li&gt;if cached → returns immediately (super fast)&lt;/li&gt;
&lt;li&gt;if not cached → fetches from origin (S3), caches it, then returns it&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  What is KMS?
&lt;/h3&gt;

&lt;p&gt;KMS (Key Management Service) manages encryption keys.&lt;/p&gt;

&lt;p&gt;When you use SSE-KMS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 stores objects encrypted&lt;/li&gt;
&lt;li&gt;when someone requests the object, S3 decrypts it (with KMS) before returning it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means encryption is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic&lt;/li&gt;
&lt;li&gt;controlled by IAM + key policies&lt;/li&gt;
&lt;li&gt;auditable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Encryption options in S3 and CloudFront
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/serv-side-encryption.html" rel="noopener noreferrer"&gt;server-side encryption (SSE)&lt;/a&gt;, and it comes in multiple flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-Side Encryption with Amazon S3-Managed Keys &lt;strong&gt;(SSE-S3)&lt;/strong&gt;, where each object is encrypted with a unique key managed by S3&lt;/li&gt;
&lt;li&gt;Server-Side Encryption with Customer Master Keys (CMKs) stored in AWS Key Management Service &lt;strong&gt;(SSE-KMS)&lt;/strong&gt;. This gives you more control and visibility into how your encryption keys are being used&lt;/li&gt;
&lt;li&gt;Server-Side Encryption with customer-provided keys &lt;strong&gt;(SSE-C)&lt;/strong&gt;, where you manage the encryption keys and S3 only manages the encryption of objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With CloudFront, you can encrypt data in transit using HTTPS, and enforce encryption policy by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redirecting &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-viewers-to-cloudfront.html" rel="noopener noreferrer"&gt;HTTP to HTTPS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Choosing minimal &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html#secure-connections-supported-ciphers" rel="noopener noreferrer"&gt;TLS version and ciphers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Selecting a domain name and its associated &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html" rel="noopener noreferrer"&gt;TLS certificate&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for serious production setups, &lt;strong&gt;SSE-KMS is the sweet spot&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How CloudFront Accesses Private S3 (OAI vs OAC)
&lt;/h2&gt;

&lt;p&gt;This is where many people get confused.&lt;/p&gt;

&lt;p&gt;Your bucket is private.&lt;/p&gt;

&lt;p&gt;So how does CloudFront fetch the objects?&lt;/p&gt;

&lt;p&gt;There are 2 approaches:&lt;/p&gt;




&lt;h3&gt;
  
  
  The Old Way: OAI (Origin Access Identity)
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;AWS has basically moved on from this.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Modern Way: OAC (Origin Access Control) ✅
&lt;/h3&gt;

&lt;p&gt;OAC is the newer, recommended approach.&lt;/p&gt;

&lt;p&gt;It uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SigV4 signing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;IAM-style access controls&lt;/li&gt;
&lt;li&gt;better security model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 If you’re doing &lt;strong&gt;SSE-KMS + CloudFront&lt;/strong&gt;, use &lt;strong&gt;OAC&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Setup (AWS Console)
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you’ll have a private S3 bucket with objects encrypted using &lt;strong&gt;SSE-KMS&lt;/strong&gt;, served securely through a CloudFront distribution.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Create Your First KMS Key&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open the AWS Console and search for &lt;strong&gt;KMS&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Customer managed keys&lt;/strong&gt;, then &lt;strong&gt;Create key&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzh1ccry3isjqj7fwoqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzh1ccry3isjqj7fwoqd.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key type:&lt;/strong&gt; Symmetric&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key usage:&lt;/strong&gt; Encrypt and decrypt&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkdrbxhx06azwiyt14e2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkdrbxhx06azwiyt14e2.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give your key a friendly label:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alias:&lt;/strong&gt; &lt;code&gt;myapp-dev-s3-key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description:&lt;/strong&gt; “SSE-KMS key for S3 content served via CloudFront”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr1tdwn9vhnkhnoyq897u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr1tdwn9vhnkhnoyq897u.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select administrators (your IAM user or deployment role), and edit the key policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljyu1jei5jop619efjoa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljyu1jei5jop619efjoa.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finish creating the key.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Create a Private S3 Bucket&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now we’ll create the bucket where your files will live. This bucket will be fully private, no accidental public access allowed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;S3 → Create bucket&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2p78fzz51w2sxfqin6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2p78fzz51w2sxfqin6c.png" alt=" " width="800" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Give it a unique name, e.g., &lt;code&gt;myapp-dev-private-assets-123456&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Bucket names must be globally unique.)&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose the &lt;strong&gt;same region&lt;/strong&gt; as your KMS key.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Block all public access:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turn on &lt;strong&gt;Block all public access&lt;/strong&gt; to prevent accidental exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable versioning (optional but recommended):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turn on versioning to protect your files from accidental deletion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth86s2d0c1pvamy99g32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth86s2d0c1pvamy99g32.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable SSE-KMS encryption:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Default encryption&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select &lt;strong&gt;SSE-KMS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Input the arn of the KMS key you created (&lt;code&gt;myapp-dev-s3-key&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Bucket Key&lt;/strong&gt; to reduce costs for large buckets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql078jd6tg81kacqa54v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql078jd6tg81kacqa54v.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;br&gt;
Now your private, encrypted bucket is ready.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Note: Bucket encryption only applies to files uploaded after it’s enabled. Files uploaded beforehand won’t be KMS-encrypted.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Give CloudFront a Key to Your Bucket (OAC)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;CloudFront needs a way to prove it’s allowed to fetch your private S3 objects. That’s what &lt;strong&gt;Origin Access Control (OAC)&lt;/strong&gt; does — think of it as giving CloudFront a secure ID card.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open &lt;strong&gt;CloudFront → Origin access → Create control setting&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4aow85swu00ihvvw2ki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4aow85swu00ihvvw2ki.png" alt=" " width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fill in:

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;myapp-dev-oac&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Origin type: &lt;strong&gt;S3&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Signing behavior: &lt;strong&gt;Always sign requests&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp67fc8tp3itg8775sbd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp67fc8tp3itg8775sbd.png" alt=" " width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudFront can now authenticate itself when fetching objects from your private bucket.&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Step 4: Create a CloudFront Distribution&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now we bring it all together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;CloudFront → Distributions → Create distribution&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnwek72sr90lhedfm2b7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffnwek72sr90lhedfm2b7.png" alt=" " width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Configure the origin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Origin domain:&lt;/strong&gt; select your S3 bucket&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Critical: Choose the bucket endpoint, e.g., myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; use the website endpoint (e.g., &lt;code&gt;s3-website.eu-west-2.amazonaws.com&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Origin access:&lt;/strong&gt; Use the OAC you just created (&lt;code&gt;myapp-dev&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fanwvkn2wgo0m0kkl4s96.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fanwvkn2wgo0m0kkl4s96.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure cache behavior:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Viewer protocol policy:&lt;/strong&gt; Redirect HTTP → HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowed methods:&lt;/strong&gt; GET, HEAD, OPTIONS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache policy:&lt;/strong&gt; CachingOptimized&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4006hc9kva17mhoryuno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4006hc9kva17mhoryuno.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Step 5: Update S3 Bucket Policy&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This step is crucial. You must update your bucket policy so CloudFront can access objects using the OAC.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;On the CloudFront distribution page, look for the &lt;strong&gt;blue banner&lt;/strong&gt;: “The S3 bucket policy needs to be updated”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Copy policy&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to &lt;strong&gt;S3 → your bucket → Permissions → Bucket policy → Edit&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Paste the copied policy and save.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example bucket policy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2008-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontServicePrincipalReadOnly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::myapp-dev-private-assets-123456/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"AWS:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudfront::123456785012:distribution/E2ABCDEFG12345"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: AWS:SourceArn will automatically include your distribution’s actual ARN.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 6: Update KMS Key Policy for CloudFront&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Since your files are &lt;strong&gt;SSE-KMS encrypted&lt;/strong&gt;, CloudFront needs permission to decrypt them. If it doesn’t, you’ll get &lt;strong&gt;403 AccessDenied&lt;/strong&gt; errors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to &lt;strong&gt;KMS → Customer managed keys → your key → Key policy → Edit&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the following statement:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"key-consolepolicy-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enable IAM User Permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;account_id&amp;gt;:root"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kms:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontDecryptThroughS3Only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"AWS:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudfront::921950937336:distribution/d303chch2n9rqs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kms:ViaService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3.eu-north-1.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AWS:SourceArn&lt;/code&gt; → your &lt;strong&gt;distribution ARN&lt;/strong&gt; (not the domain)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kms:ViaService&lt;/code&gt; → the region of your S3 bucket&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 7: Wait for Deployment&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;CloudFront needs time to propagate changes globally (5–15 minutes).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distribution status: &lt;strong&gt;Deploying → Enabled&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;After policy updates, wait 2–3 minutes for propagation&lt;/li&gt;
&lt;li&gt;Optional: Create a cache invalidation (&lt;code&gt;/*&lt;/code&gt;) to test immediately&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Step 8: Test Everything&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Upload a test file&lt;/strong&gt; (&lt;code&gt;test.jpg&lt;/code&gt;) to S3&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify SSE-KMS encryption under &lt;strong&gt;Properties → Server-side encryption settings&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Test CloudFront URL&lt;/strong&gt; (should succeed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://d1234abcdef.cloudfront.net/test.jpg

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;HTTP/2 200 OK&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test direct S3 URL or hit the endpoint on a browser&lt;/strong&gt; (should fail):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://myapp-dev-private-assets-123456.s3.eu-west-2.amazonaws.com/test.jpg

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;HTTP/1.1 403 Forbidden&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qii9ljoxuqfx1sqe98b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qii9ljoxuqfx1sqe98b.png" alt=" " width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ Perfect! CloudFront is the only public access point.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Common Pitfalls&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;403 AccessDenied from CloudFront&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Wrong &lt;code&gt;AWS:SourceArn&lt;/code&gt; in KMS key policy&lt;/li&gt;
&lt;li&gt;Bucket policy doesn’t match OAC&lt;/li&gt;
&lt;li&gt;KMS key policy missing CloudFront permissions&lt;/li&gt;
&lt;li&gt;Policy propagation delay&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files uploaded before encryption enabled&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Old files won’t be KMS-encrypted&lt;/li&gt;
&lt;li&gt;Fix: Re-upload or copy files to the same bucket with SSE-KMS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong S3 Origin Endpoint&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Use the bucket endpoint, not the website endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront caching old errors&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Invalidate cache: &lt;code&gt;/*&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>aws</category>
      <category>cloud</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploying Adminer on ECS to Access a Private RDS Database (Terraform)</title>
      <dc:creator>Abdulrasheed Abdulazeez</dc:creator>
      <pubDate>Wed, 28 Jan 2026 11:27:41 +0000</pubDate>
      <link>https://dev.to/abdulrasheed_abdulazeez_7/deploying-adminer-on-ecs-to-access-a-private-rds-database-terraform-262p</link>
      <guid>https://dev.to/abdulrasheed_abdulazeez_7/deploying-adminer-on-ecs-to-access-a-private-rds-database-terraform-262p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fypf036swvzo14s3tzco4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fypf036swvzo14s3tzco4.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently, I worked on a project where we set up &lt;strong&gt;Elastic Container Service (ECS)&lt;/strong&gt; and an &lt;strong&gt;RDS PostgreSQL database&lt;/strong&gt; on AWS. For security reasons, both were deployed in a &lt;strong&gt;private subnet&lt;/strong&gt;, making them inaccessible from the public internet.&lt;/p&gt;

&lt;p&gt;The challenge was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How can administrators access the database to view tables, inspect data, and perform troubleshooting when it’s not publicly accessible?&lt;/li&gt;
&lt;li&gt;If developers cannot directly connect using desktop database clients, or connect to the database endpoint locally when they run the service, how do we make it convenient for them to run quick fix and debugging?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where &lt;strong&gt;Adminer&lt;/strong&gt; becomes very useful.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Traditional Approaches Fall Short
&lt;/h1&gt;

&lt;p&gt;When i first thought about how to let administrators access a database in a private subnet, i explored the usual options and quickly realized each one had serious drawbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bastion Host (Jump Server)
&lt;/h3&gt;

&lt;p&gt;One common approach is to spin up a &lt;strong&gt;bastion host&lt;/strong&gt;, or jump server, in a public subnet. The idea is simple, admins SSH into the bastion, then connect to the private database.&lt;/p&gt;

&lt;p&gt;But in practice, it’s a hassle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to manage EC2 instances and keep them secure.&lt;/li&gt;
&lt;li&gt;SSH keys must be distributed and rotated.&lt;/li&gt;
&lt;li&gt;Every extra server adds cost and increases the attack surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small team or occasional access, this felt like overkill.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPN (Client or Site-to-Site)
&lt;/h3&gt;

&lt;p&gt;Next, i considered a &lt;strong&gt;VPN&lt;/strong&gt;. It can give admins direct access to the private network, which sounds ideal.&lt;/p&gt;

&lt;p&gt;The problem? Setting up and maintaining a VPN is complicated. There’s software to install, configurations to manage, and costs to consider, especially if it’s just for occasional database access. For our needs, it was more effort than it was worth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Public RDS Access
&lt;/h3&gt;

&lt;p&gt;Some teams might be tempted to just open up the RDS instance to the internet. It’s quick, and you can connect from anywhere. I knew immediately that this was not what the team wanted, exposing a production database publicly is a serious security risk. Not an option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port Forwarding via SSM
&lt;/h3&gt;

&lt;p&gt;AWS offers &lt;strong&gt;Session Manager port forwarding&lt;/strong&gt; via SSM as another way to reach private resources. While technically feasible, it’s not exactly user-friendly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sessions are temporary and can drop unexpectedly.&lt;/li&gt;
&lt;li&gt;Extra tooling and scripts are often required.&lt;/li&gt;
&lt;li&gt;Collaboration among team members becomes awkward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time i weighed all these options, it became clear i needed a solution that was &lt;strong&gt;simple, secure, and easy for anyone on the team to use&lt;/strong&gt;. That’s when &lt;strong&gt;Adminer&lt;/strong&gt; entered the picture. By containerizing Adminer, we could keep it &lt;strong&gt;inside the same VPC and private subnet&lt;/strong&gt; as our RDS database, giving our admins secure access without opening the database to the public internet.&lt;/p&gt;




&lt;h1&gt;
  
  
  What is Adminer
&lt;/h1&gt;

&lt;p&gt;Adminer is a full-featured database management tool built with PHP. It’s similar to phpMyAdmin, but much lighter and faster, making it ideal for modern cloud environments where you want something quick to deploy and easy to scale down or remove when you’re done.&lt;/p&gt;

&lt;p&gt;It supports multiple database engines such as &lt;strong&gt;PostgreSQL, MySQL, MS SQL, Oracle&lt;/strong&gt;, and more, and it comes with a clean, straightforward interface for running queries, browsing tables, and inspecting data.&lt;/p&gt;

&lt;p&gt;Adminer is especially useful when your database lives in a &lt;strong&gt;private subnet&lt;/strong&gt; and you still need controlled access for troubleshooting. Instead of exposing the database publicly or forcing every developer to configure local database clients, Adminer provides a secure, browser-based way to inspect and debug.&lt;/p&gt;

&lt;p&gt;Think of it as a web alternative to tools like &lt;strong&gt;pgAdmin, DBeaver, or TablePlus&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;strong&gt;Architecture&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk00pgpmgurdbacbl5v7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk00pgpmgurdbacbl5v7n.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Here’s a simplified view of how it works:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Adminer lives in the same ECS cluster and subnet as our app&lt;/li&gt;
&lt;li&gt;It reuses the existing ALB, so we don’t need extra servers.&lt;/li&gt;
&lt;li&gt;We can turn it off, scale it down, or remove it whenever we want.&lt;/li&gt;
&lt;li&gt;And admins just need a browser, no local tools required.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Implementation:
&lt;/h1&gt;

&lt;p&gt;For this setup, I used the official Adminer Docker image (&lt;code&gt;adminer:latest&lt;/code&gt;). It’s lightweight (~90MB), ships with PHP + Apache, exposes port 8080, and supports PostgreSQL out of the box.&lt;/p&gt;

&lt;p&gt;Adminer runs as an ECS service inside the same private network as the application, while access is provided securely through the existing public ALB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adminer ECS Task Definition
&lt;/h2&gt;

&lt;p&gt;Adminer is deployed as an ECS task running on &lt;strong&gt;EC2 launch type&lt;/strong&gt;, using minimal resources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU:&lt;/strong&gt; 128 units&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; 256MB&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container port:&lt;/strong&gt; 8080&lt;/p&gt;

&lt;p&gt;Adminer is configured using environment variables so it knows which database to connect to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ADMINER_DEFAULT_SERVER&lt;/code&gt; → RDS endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADMINER_DEFAULT_DB_DRIVER&lt;/code&gt; → &lt;code&gt;pgsql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADMINER_DEFAULT_DB_NAME&lt;/code&gt; → database name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADMINER_DESIGN&lt;/code&gt; → optional UI theme
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Adminer&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt; &lt;span class="nx"&gt;Definition&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_ecs_task_definition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_adminer&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp-adminer-${var.environment}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bridge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;task_role_arn&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer:latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;essential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="nx"&gt;memory&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
      &lt;span class="nx"&gt;cpu&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
            &lt;span class="nx"&gt;portMappings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;containerPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
          &lt;span class="nx"&gt;hostPort&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
          &lt;span class="nx"&gt;protocol&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMINER_DEFAULT_SERVER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_host&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMINER_DEFAULT_DB_DRIVER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pgsql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMINER_DEFAULT_DB_NAME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_name&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMINER_DESIGN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pepa-linha-dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;logConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;logDriver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awslogs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;awslogs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
          &lt;span class="nx"&gt;awslogs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
          &lt;span class="nx"&gt;awslogs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;healthCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CMD-SHELL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wget --no-verbose --tries=1 --spider &amp;lt;http://localhost:8080/&amp;gt; || exit 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;interval&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
        &lt;span class="nx"&gt;timeout&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="nx"&gt;retries&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="nx"&gt;startPeriod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp-adminer-${var.environment}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adminer ECS Service (Behind the ALB)
&lt;/h2&gt;

&lt;p&gt;The ECS service ensures Adminer stays running and is reachable through the ALB.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service name:&lt;/strong&gt; &lt;code&gt;myapp-adminer-dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desired count:&lt;/strong&gt; 1&lt;/li&gt;
&lt;li&gt;Registered into the existing ALB target group&lt;/li&gt;
&lt;li&gt;Health checks handled by the ALB
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Adminer&lt;/span&gt; &lt;span class="nx"&gt;ECS&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws_ecs_service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable_adminer&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp-adminer-${var.environment}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_definition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_task_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adminer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;desired_count&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;launch_type&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EC2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_maximum_percent&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_minimum_healthy_percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adminer_target_group_arn&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;container_port&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;deployment_circuit_breaker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enable&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;rollback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ordered_placement_strategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instanceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ordered_placement_strategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attribute:ecs.availability-zone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp-adminer-${var.environment}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adminer_target_group_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Routing With ALB
&lt;/h2&gt;

&lt;p&gt;Adminer is routed using a &lt;strong&gt;path-based rule&lt;/strong&gt;, so only requests matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/adminer*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are forwarded to the Adminer target group.&lt;/p&gt;

&lt;p&gt;This keeps the application routing unchanged and prevents interference with normal app traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;No extra security groups were required beyond what already existed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ALB handles HTTP ingress&lt;/strong&gt; to Adminer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS can reach RDS&lt;/strong&gt; on port &lt;strong&gt;5432&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database authentication is still required&lt;/strong&gt;, meaning Adminer does not bypass credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Secrets Manager (Created Using AWS CLI)
&lt;/h2&gt;

&lt;p&gt;In my setup, database credentials are stored in &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt;, and the same secret is reused by both the application and Adminer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create secret
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="nx"&gt;secretsmanager&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;myapp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/db_credentials &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PostgreSQL credentials for Adminer + app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{
    "username": "db_user",
    "password": "db_password"
  }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Update secret value&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="nx"&gt;secretsmanager&lt;/span&gt; &lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="nx"&gt;myapp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/db_credentials &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{
    "username": "db_user",
    "password": "new_password"
  }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Using Adminer
&lt;/h1&gt;

&lt;p&gt;Once deployed, access is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;alb-dns-name&amp;gt;/adminer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The login screen appears with prefilled values:&lt;/li&gt;
&lt;li&gt;System: PostgreSQL&lt;/li&gt;
&lt;li&gt;Server: RDS endpoint&lt;/li&gt;
&lt;li&gt;Database: your DB name&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajw7wwcdbcyzifz13dev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajw7wwcdbcyzifz13dev.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter credentials and log in.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Deployment Flow
&lt;/h1&gt;

&lt;p&gt;Everything is managed as &lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt; using Terraform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Create task definition&lt;/li&gt;
&lt;li&gt;Create ECS service&lt;/li&gt;
&lt;li&gt;Create ALB target group + listener rule&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;2. Deploy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform apply&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Verify&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECS task is running&lt;/li&gt;
&lt;li&gt;target group is healthy&lt;/li&gt;
&lt;li&gt;Adminer UI loads successfully&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;By running Adminer inside ECS, it gave our admins &lt;strong&gt;secure, browser-based access to a private RDS database&lt;/strong&gt; without compromising our VPC or infrastructure.&lt;/p&gt;

&lt;p&gt;It’s fast, safe, and easy to scale or remove, a perfect solution for teams that need occasional database access without the headaches of VPNs or bastion hosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adminer may be small, but in our setup, it became a powerful tool that makes life easier for developers.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
